Java线程
Java爱好者 第 1 页 http://www.javafan.net
Java线程
本教程来源互连网,仅供学习,
版权归原作者及其出版商所有。
Java线程
Java爱好者 第 2 页 http://www.javafan.net
第一章 关于本教程
本教程有什么内容?
本教程研究了线程的基础知识 — 线程是什么、线程为什么有用以及怎么开始编写使用线程的简单
程序。
我们还将研究更复杂的、使用线程的应用程序的基本构件 — 如何在线程之间交换数据、如何控制
线程以及线程如何互相通信。
我应该学习这个教程吗?
本教程适用于拥有丰富 Java 语言应用知识,但又没有多少多线程或并发性经验的 Java 程序员。
学习完本教程之后,您应该可以编写一个使用线程的简单程序。您还应该可以阅读并理解以简单方
法使用线程的程序。
关于作者
Brian Goetz 是 developerWorks Java 技术专区的一名定期专栏作家,而且他在过去的 15 年里一
直是专业软件开发人员。他是 Quiotix 的首席顾问,这是一家位于加利福尼亚州洛斯阿尔托斯市
(Los Altos)的软件开发和咨询公司。
在流行的业界出版物上可以看到 Brian 发
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
和即将发表的文章。
可以通过 brian@quiotix.com 联系 Brian。
Java线程
Java爱好者 第 3 页 http://www.javafan.net
第二章 线程基础
什么是线程?
几乎每种操作系统都支持进程的概念 —— 进程就是在某种程度上相互隔离的、独立运行的程序。
线程化是允许多个活动共存于一个进程中的工具。大多数现代的操作系统都支持线程,而且线程的
概念以各种形式已存在了好多年。Java 是第一个在语言本身中显式地包含线程的主流编程语言,它
没有把线程化看作是底层操作系统的工具。
有时候,线程也称作轻量级进程。就象进程一样,线程在程序中是独立的、并发的执行路径,每个
线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的
线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。
进程可以支持多个线程,它们看似同时执行,但互相之间并不同步。一个进程中的多个线程共享相
同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。
尽管这让线程之间共享信息变得更容易,但您必须小心,确保它们不会妨碍同一进程里的其它线程。
Java 线程工具和 API 看似简单。但是,编写有效使用线程的复杂程序并不十分容易。因为有多个
线程共存在相同的内存空间中并共享相同的变量,所以您必须小心,确保您的线程不会互相干扰。
每个 Java 程序都使用线程
每个 Java 程序都至少有一个线程 — 主线程。当一个 Java 程序启动时,JVM 会创建主线程,并
在该线程中调用程序的 main() 方法。
JVM 还创建了其它线程,您通常都看不到它们 — 例如,与垃圾收集、对象终止和其它 JVM 内务处
理任务相关的线程。其它工具也创建线程,如 AWT(抽象窗口工具箱(Abstract Windowing Toolkit))
或 Swing UI 工具箱、servlet 容器、应用程序服务器和 RMI(远程方法调用(Remote Method
Invocation))。
为什么使用线程?
Java线程
Java爱好者 第 4 页 http://www.javafan.net
在 Java 程序中使用线程有许多原因。如果您使用 Swing、servlet、RMI 或 Enterprise JavaBeans
(EJB)技术,您也许没有意识到您已经在使用线程了。
使用线程的一些原因是它们可以帮助:
· 使 UI 响应更快
· 利用多处理器系统
· 简化建模
· 执行异步或后台处理
响应更快的 UI
事件驱动的 UI 工具箱(如 AWT 和 Swing)有一个事件线程,它处理 UI 事件,如击键或鼠标点击。
AWT 和 Swing 程序把事件侦听器与 UI 对象连接。当特定事件(如单击了某个按钮)发生时,这些
侦听器会得到通知。事件侦听器是在 AWT 事件线程中调用的。
如果事件侦听器要执行持续很久的任务,如检查一个大文档中的拼写,事件线程将忙于运行拼写检
查器,所以在完成事件侦听器之前,就不能处理额外的 UI 事件。这就会使程序看来似乎停滞了,
让用户不知所措。
要避免使 UI 延迟响应,事件侦听器应该把较长的任务放到另一个线程中,这样 AWT 线程在任务的
执行过程中就可以继续处理 UI 事件(包括取消正在执行的长时间运行任务的请求)。
利用多处理器系统
多处理器(MP)系统比过去更普及了。以前只能在大型数据中心和科学计算设施中才能找到它们。
现在许多低端服务器系统 — 甚至是一些台式机系统 — 都有多个处理器。
现代操作系统,包括 Linux、Solaris 和 Windows NT/2000,都可以利用多个处理器并调度线程在
任何可用的处理器上执行。
Java线程
Java爱好者 第 5 页 http://www.javafan.net
调度的基本单位通常是线程;如果某个程序只有一个活动的线程,它一次只能在一个处理器上运行。
如果某个程序有多个活动线程,那么可以同时调度多个线程。在精心
设计
领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计
的程序中,使用多个线程
可以提高程序吞吐量和性能。
简化建模
在某些情况下,使用线程可以使程序编写和维护起来更简单。考虑一个仿真应用程序,您要在其中
模拟多个实体之间的交互作用。给每个实体一个自己的线程可以使许多仿真和对应用程序的建模大
大简化。
另一个适合使用单独线程来简化程序的示例是在一个应用程序有多个独立的事件驱动的组件的时
候。例如,一个应用程序可能有这样一个组件,该组件在某个事件之后用秒数倒计时,并更新屏幕
显示。与其让一个主循环定期检查时间并更新显示,不如让一个线程什么也不做,一直休眠,直到
某一段时间后,更新屏幕上的计数器,这样更简单,而且不容易出错。这样,主线程就根本无需担
心计时器。
异步或后台处理
服务器应用程序从远程来源(如套接字)获取输入。当读取套接字时,如果当前没有可用数据,那
么对 SocketInputStream.read() 的调用将会阻塞,直到有可用数据为止。
如果单线程程序要读取套接字,而套接字另一端的实体并未发送任何数据,那么该程序只会永远等
待,而不执行其它处理。相反,程序可以轮询套接字,查看是否有可用数据,但通常不会使用这种
做法,因为会影响性能。
但是,如果您创建了一个线程来读取套接字,那么当这个线程等待套接字中的输入时,主线程就可
以执行其它任务。您甚至可以创建多个线程,这样就可以同时读取多个套接字。这样,当有可用数
据时,您会迅速得到通知(因为正在等待的线程被唤醒),而不必经常轮询以检查是否有可用数据。
使用线程等待套接字的代码也比轮询更简单、更不易出错。
Java线程
Java爱好者 第 6 页 http://www.javafan.net
简单,但有时有风险
虽然 Java 线程工具非常易于使用,但当您创建多线程程序时,应该尽量避免一些风险。
当多个线程访问同一数据项(如静态字段、可全局访问对象的实例字段或共享集合)时,需要确保
它们协调了对数据的访问,这样它们都可以看到数据的一致视图,而且相互不会干扰另一方的更改。
为了实现这个目的,Java 语言提供了两个关键字:synchronized 和 volatile。我们将稍后在本教
程中研究这些关键字的用途和意义。
当从多个线程中访问变量时,必须确保对该访问正确地进行了同步。对于简单变量,将变量声明成
volatile 也许就足够了,但在大多数情况下,需要使用同步。
如果您将要使用同步来保护对共享变量的访问,那么必须确保在程序中所有访问该变量的地方都使
用同步。
不要做过头
虽然线程可以大大简化许多类型的应用程序,过度使用线程可能会危及程序的性能及其可维护性。
线程消耗了资源。因此,在不降低性能的情况下,可以创建的线程的数量是有限制的。
尤其在单处理器系统中,使用多个线程不会使主要消耗 CPU 资源的程序运行得更快。
示例:使用一个线程用于计时,并使用另一个线程完成工作
以下示例使用两个线程,一个用于计时,一个用于执行实际工作。主线程使用非常简单的算法计算
素数。
在它启动之前,它创建并启动一个计时器线程,这个线程会休眠十秒钟,然后设置一个主线程要检
查的标志。十秒钟之后,主线程将停止。请注意,共享标志被声明成 volatile。
/**
Java线程
Java爱好者 第 7 页 http://www.javafan.net
* CalculatePrimes -- calculate as many primes as we can in ten seconds
*/
public class CalculatePrimes extends Thread {
public static final int MAX_PRIMES = 1000000;
public static final int TEN_SECONDS = 10000;
public volatile boolean finished = false;
public void run() {
int[] primes = new int[MAX_PRIMES];
int count = 0;
for (int i=2; count
目录
工贸企业有限空间作业目录特种设备作业人员作业种类与目录特种设备作业人员目录1类医疗器械目录高值医用耗材参考目录
接口(Java Naming
and Directory Interface (JNDI))名称空间。当远程客户机调用其中的一个方法时,该方法会在
什么线程中执行呢?
实现 RMI 对象的常用方法是继承 UnicastRemoteObject。在构造 UnicastRemoteObject 时,会初
始化用于分派远程方法调用的基础结构。这包括用于接收远程调用请求的套接字侦听器,和一个或
多个执行远程请求的线程。
所以,当接收到执行 RMI 方法的请求时,这些方法将在 RMI 管理的线程中执行。
小结
线程通过几种机制进入 Java 程序。除了用 Thread 构造器中显式创建线程之外,还可以用许多其
它机制创建线程:
· AWT 和 Swing
· RMI
· java.util.TimerTask 工具
· servlet 和 JSP 技术
Java线程
Java爱好者 第 17 页 http://www.javafan.net
第五章 共享对数据的访问
共享变量
要使多个线程在一个程序中有用,它们必须有某种方法可以互相通信或共享它们的结果。
让线程共享其结果的最简单方法是使用共享变量。它们还应该使用同步来确保值从一个线程正确传
播到另一个线程,以及防止当一个线程正在更新一些相关数据项时,另一个线程看到不一致的中间
结果。
线程基础中计算素数的示例使用了一个共享布尔变量,用于表示指定的时间段已经过去了。这说明
了在线程间共享数据最简单的形式是:轮询共享变量以查看另一个线程是否已经完成执行某项任务。
存在于同一个内存空间中的所有线程
正如前面讨论过的,线程与进程有许多共同点,不同的是线程与同一进程中的其它线程共享相同的
进程上下文,包括内存。这非常便利,但也有重大责任。只要访问共享变量(静态或实例字段),
线程就可以方便地互相交换数据,但线程还必须确保它们以受控的方式访问共享变量,以免它们互
相干扰对方的更改。
任何线程可以访问所有其作用域内的变量,就象主线程可以访问该变量一样。素数示例使用了一个
公用实例字段,叫做 finished,用于表示已经过了指定的时间。当计时器过期时,一个线程会写这
个字段;另一个线程会定期读取这个字段,以检查它是否应该停止。注:这个字段被声明成 volatile,
这对于这个程序的正确运行非常重要。在本章的后面,我们将看到原因。
受控访问的同步
为了确保可以在线程之间以受控方式共享数据,Java 语言提供了两个关键字:synchronized 和
volatile。
Java线程
Java爱好者 第 18 页 http://www.javafan.net
Synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥,
mutual exclusion 或者说 mutex),而且它确保了一个线程更改的数据对于其它线程是可见的(更
改的可见性)。
如果没有同步,数据很容易就处于不一致状态。例如,如果一个线程正在更新两个相关值(比如,
粒子的位置和速率),而另一个线程正在读取这两个值,有可能在第一个线程只写了一个值,还没
有写另一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值。同步让我们可
以定义必须原子地运行的代码块,这样对于其他线程而言,它们要么都执行,要么都不执行。
同步的原子执行或互斥方面类似于其它操作环境中的临界段的概念。
确保共享数据更改的可见性
同步可以让我们确保线程看到一致的内存视图。
处理器可以使用高速缓存加速对内存的访问(或者编译器可以将值存储到寄存器中以便进行更快的
访问)。在一些多处理器体系结构上,如果在一个处理器的高速缓存中修改了内存位置,没有必要
让其它处理器看到这一修改,直到刷新了写入器的高速缓存并且使读取器的高速缓存无效。
这表示在这样的系统上,对于同一变量,在两个不同处理器上执行的两个线程可能会看到两个不同
的值!这听起来很吓人,但它却很常见。它只是表示在访问其它线程使用或修改的数据时,必须遵
循某些规则。
Volatile 比同步更简单,只适合于控制对基本变量(整数、布尔变量等)的单个实例的访问。当一
个变量被声明成 volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对
该变量的读取也都绕过高速缓存,直接取自主内存。这表示所有线程在任何时候看到的 volatile 变
量值都相同。
如果没有正确的同步,线程可能会看到旧的变量值,或者引起其它形式的数据损坏。
Java线程
Java爱好者 第 19 页 http://www.javafan.net
用锁保护的原子代码块
Volatile 对于确保每个线程看到最新的变量值非常有用,但有时我们需要保护比较大的代码片段,
如涉及更新多个变量的片段。
同步使用监控器(monitor)或锁的概念,以协调对特定代码块的访问。
每个 Java 对象都有一个相关的锁。同一时间只能有一个线程持有 Java 锁。当线程进入
synchronized 代码块时,线程会阻塞并等待,直到锁可用,当它可用时,就会获得这个锁,然后执
行代码块。当控制退出受保护的代码块时,即到达了代码块末尾或者抛出了没有在 synchronized 块
中捕获的异常时,它就会释放该锁。
这样,每次只有一个线程可以执行受给定监控器保护的代码块。从其它线程的角度看,该代码块可
以看作是原子的,它要么全部执行,要么根本不执行。
简单的同步示例
使用 synchronized 块可以让您将一组相关更新作为一个集合来执行,而不必担心其它线程中断或
看到计算的中间结果。以下示例代码将打印“1 0”或“0 1”。如果没有同步,它还会打印“1 1”
(或“0 0”,随便您信不信)。
public class SyncExample {
private static lockObject = new Object();
private static class Thread1 extends Thread {
public void run() {
synchronized (lockObject) {
x = y = 0;
System.out.println(x);
}
}
}
private static class Thread2 extends Thread {
public void run() {
synchronized (lockObject) {
Java线程
Java爱好者 第 20 页 http://www.javafan.net
x = y = 1;
System.out.println(y);
}
}
}
public static void main(String[] args) {
new Thread1().run();
new Thread2().run();
}
}
在这两个线程中都必须使用同步,以便使这个程序正确工作。
Java 锁定
Java 锁定合并了一种互斥形式。每次只有一个线程可以持有锁。锁用于保护代码块或整个方法,必
须记住是锁的身份保护了代码块,而不是代码块本身,这一点很重要。一个锁可以保护许多代码块
或方法。
反之,仅仅因为代码块由锁保护并不表示两个线程不能同时执行该代码块。它只表示如果两个线程
正在等待相同的锁,则它们不能同时执行该代码。
在以下示例中,两个线程可以同时不受限制地执行 setLastAccess() 中的 synchronized 块,因为
每个线程有一个不同的 thingie 值。因此,synchronized 代码块受到两个正在执行的线程中不同
锁的保护。
public class SyncExample {
public static class Thingie {
private Date lastAccess;
public synchronized void setLastAccess(Date date) {
this.lastAccess = date;
}
}
Java线程
Java爱好者 第 21 页 http://www.javafan.net
public static class MyThread extends Thread {
private Thingie thingie;
public MyThread(Thingie thingie) {
this.thingie = thingie;
}
public void run() {
thingie.setLastAccess(new Date());
}
}
public static void main() {
Thingie thingie1 = new Thingie(),
thingie2 = new Thingie();
new MyThread(thingie1).start();
new MyThread(thingie2).start();
}
}
同步的方法
创建 synchronized 块的最简单方法是将方法声明成 synchronized。这表示在进入方法主体之前,
调用者必须获得锁:
public class Point {
public synchronized void setXY(int x, int y) {
this.x = x;
this.y = y;
}
}
对于普通的 synchronized方法,这个锁是一个对象,将针对它调用方法。对于静态 synchronized
方法,这个锁是与 Class 对象相关的监控器,在该对象中声明了方法。
Java线程
Java爱好者 第 22 页 http://www.javafan.net
仅仅因为 setXY() 被声明成 synchronized 并不表示两个不同的线程不能同时执行 setXY(),只要
它们调用不同的 Point 实例的 setXY() 就可同时执行。对于一个 Point 实例,一次只能有一个线
程执行 setXY(),或 Point 的任何其它 synchronized 方法。
同步的块
synchronized 块的语法比 synchronized 方法稍微复杂一点,因为还需要显式地指定锁要保护哪个
块。Point 的以下版本等价于前一页中显示的版本:
public class Point {
public void setXY(int x, int y) {
synchronized (this) {
this.x = x;
this.y = y;
}
}
}
使用 this 引用作为锁很常见,但这并不是必需的。这表示该代码块将与这个类中的 synchronized
方法使用同一个锁。
由于同步防止了多个线程同时执行一个代码块,因此性能上就有问题,即使是在单处理器系统上。
最好在尽可能最小的需要保护的代码块上使用同步。
访问局部(基于堆栈的)变量从来不需要受到保护,因为它们只能被自己所属的线程访问。
大多数类并没有同步
因为同步会带来小小的性能损失,大多数通用类,如 java.util 中的 Collection 类,不在内部使
用同步。这表示在没有附加同步的情况下,不能在多个线程中使用诸如 HashMap 这样的类。
Java线程
Java爱好者 第 23 页 http://www.javafan.net
通过每次访问共享集合中的方法时使用同步,可以在多线程应用程序中使用 Collection 类。对于
任何给定的集合,每次必须用同一个锁进行同步。通常可以选择集合对象本身作为锁。
下一页中的示例类 SimpleCache 显示了如何使用 HashMap 以线程安全的方式提供高速缓存。但是,
通常适当的同步并不只是意味着同步每个方法。
Collections 类提供了一组便利的用于 List、Map 和 Set 接口的封装器。您可以用
Collections.synchronizedMap 封装 Map,它将确保所有对该映射的访问都被正确同步。
如果类的文档没有说明它是线程安全的,那么您必须假设它不是。
示例:简单的线程安全的高速缓存
如以下代码样本所示,SimpleCache.java 使用 HashMap 为对象装入器提供了一个简单的高速缓存。
load() 方法知道怎样按对象的键装入对象。在一次装入对象之后,该对象就被存储到高速缓存中,