第 14 章 多线程
本章内容:
* 什么是线程
* 中断线程
* 线程状态
* 线程属性
* 同步
* 阻塞队列
* 线程安全的集合
* Collable与Future
* 执行器
* 同步器
* 线程与Swing
- 通常,每一个任务称为一个线程(thread),它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。
- 多进程与多线程有哪些区别呢?本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间的通信更有效、更容易。此外,在有些操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多。
14.1 什么是线程
- Thread 类的静态 sleep 方法将暂停给定的毫秒数。调用 Thread.sleep 不会创建一个新线程, sleep 是 Thread 类的静态方法,用于暂停当前线程的活动。 sleep 方法可以抛出一个 InterruptedException 异常。
- java.lang.Thread 1.0
- static void sleep(long millis)
休眠给定的毫秒数。
参数:millis 休眠的毫秒数
- static void sleep(long millis)
- 不要调用 Thread 类或 Runnable 对象的 run 方法。直接调用 run 方法,只会执行同一个线程中的任务,并不会启动新线程。应该调用 Thread.start 方法。这个方法将创建一个执行 run 方法的新线程。
- java.lang.Thread 1.0
- Thread(Runnable target)
构造一个新线程,用于调用给定target的run()方法。 - void start()
启动这个线程,将引发调用run()方法。这个方法将立即返回,并且新线程将并行运行。 - void run()
调用关联Runnable的run方法。
- Thread(Runnable target)
- java.lang.Runnable 1.0
- void run()
必须覆盖这个方法,并在这个方法中提供所要执行的任务指令。
- void run()
14.2 中断线程
- 当线程的 run 方法执行方法体重最后一条语句后,并经由执行 return 语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。在Java的早期版本中,还有一个 stop 方法,其他线程可以调用它终止线程。但是,这个方法现在已经被弃用了。
- 有一种可以强制线程终止的方法。然而, interrupt 方法可以用来请求终止线程。
- 当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。这是每一个线程都具有的 boolean 标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。
- 调用 Thread.currentThread().isInterrputed() 方法获得当前线程的中断状态是否被置位。但是,如果线程被阻塞,就无法检测中断状态。这是产生 InterruptedException 异常的地方。当在一个被阻塞的线程(调用 sleep 或 wait )上调用 interrupt 方法时,阻塞调用将会被 Interrupt Exception 异常中断(存在不能被中断的阻塞 I/O 调用,应该考虑选择可中断的调用)。
- 没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后,继续执行,而不理会中断。但是,更普通的情况是,线程将简单地将中断作为一个终止的请求。
- 如果在每次工作迭代之后都调用 sleep 方法(或者其他的可中断方法), isInterrpted 检测既没有必要也没有用处。如果在种蒜状态被置位时调用 sleep 方法,它不会休眠。相反,它将清除这一状态(!)并抛出 InterrputedException 。因此,如果你的循环调用 sleep ,不会检测中断状态,相反,需要捕获 InterrputedException 异常。
- 有两个非常类似的方法, interrupted 和 isInterrupted 。 Interrupted 方法是一个静态方法,它检测当前的线程是否被中断。而且,调用 interrupted 方法会清除该线程的中断状态。另一方面, isInterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。
- java.lang.Thread 1.0
- void interrupt()
向线程发送中断请求。线程的中断状态将设置为 true 。如果目前该线程被一个 sleep 调用阻塞,那么, InterruptedException 异常被抛出。 - static boolean interrupted()
测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生副作用-它将当前线程的中断状态重置为false。 - boolean isInterrupted()
测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。 - static Thread currentThread()
返回代表当前执行线程的 Thread 对象。
- void interrupt()
14.3 线程状态
- 线程可以有如下6种状态:
- New (新创建)
- Runnable (可运行)
- Blocked (被阻塞)
- Waiting (等待)
- Timed waiting (计时等待)
- Terminated (被终止)
14.3.1 新创建线程
- 当用new操作符创建一个新线程,如 new Thread(r) ,该线程还没有开始运行。这意味着它的状态是 new 。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基本工作要做。
14.3.2 可运行线程
- 一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。( Java 的规范说明没有将它作为一个单独状态。一个正在运行中的线程仍然处于可运行状态。)
- 一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。当选择下一个线程时,操作系统考虑线程的优先级。
- 在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行)。
14.3.3 被阻塞线程和等待线程
- 当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的。
- 当一个线程试图获取一个内部的对象锁(而不是 java.util.concurrect 库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
- 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 库中的 Lock 或 Condition 时,就会出现这种情况。实际上,被阻塞状态与等待状态是由很大不同的。
- 有几个方法有一个超时参数。调用它们导致线程进入计时等待( timed waiting )状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.wait 、 Thread.join 、 Lock.tryLock 以及 Condition.awit 的计时版。
- 当一个线程被阻塞或等待时(或终止时),另一个线程被调度为运行状态。当一个线程被重新激活(例如,因为超时期满或成功地获得一个锁),调度器检查它是否具有比当前运行线程更高的优先级。如果是这样,调度器从当前运行线程中挑选一个,剥夺其运行权,选择一个新的线程运行。
- 线程状态图
14.3.4 被终止的线程
- 线程因如下两个原因之一而被终止:
- 因为 run 方法正常退出而自然死亡。
- 因为一个没有捕获的异常终止了 run 方法而意外死亡。
特别是,可以调用线程的 stop 方法杀死一个线程。该方法抛出 ThreadDeath 错误对象,由此杀死线程。但是, stop 方法已过时,不要在自己的代码中调用这个方法。
- java.lang.Thread 1.0
- void join()
等待终止指定的线程。 - void join(long millis)
等待指定的线程死亡或者经过指定的毫秒数。 - Thread.State getState() 5.0
得到这一线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING或TERMINATED之一。 - void stop()
停止该线程。这一方法已过时。 - void suspend()
暂停这一线程的执行。这一方法已过时。 - void resume()
恢复线程。这一方法仅仅在调用suspend()之后调用。这一方法已过时。
- void join()
14.4 线程属性
- 线程的各种属性,其中包括:线程优先级、守护线程、线程组以及处理未捕获异常的处理器。
14.4.1 线程优先级
- 在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。可以将优先级设置为在 MIN_PRIORITY (在 Thread 类中定义为 1 )与 MAX_PRIORITY (定义为 10 )之间的任何值。 NORM_PRIORITY 被定义为 5 。
- 每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时, Java 线程的优先级被映射到宿主主机平台的优先级上,优先级个数也许更多,也许更少。
Windows 有 7 个优先级别。一些 Java 优先级将映射到相同的操作系统优先级。在 Sun 为 Linux 提供的 Java 虚拟机,线程的优先级被忽略-所有线程具有相同的优先级。 - java.lang.Thread 1.0
- void setPriority(int newPriority)
设置线程的优先级。优先级必须在Thread.MIN_PRIORITY与Thread.MAX_PRIORITY之间。一般使用Thread.NORM_PRIORITY优先级。 - static int MIN_PRIORITY
线程的最小优先级。最小优先级的值为1。 - static int NORM_PRIORITY
线程的默认优先级。默认优先级为5。 - static int MAX_PRIORITY
线程的最高优先级。最高优先级的值为10。 - static void yield()
导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。
- void setPriority(int newPriority)
14.4.2 守护线程
- 可以通过
t.setDaemon(true)
将线程转换为守护线程( daemon thread )。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。 - 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
- java.lang.Threed 1.0
- void setDaemon(boolean isDaemon)
标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。
- void setDaemon(boolean isDaemon)
14.4.3 未捕获异常处理器
- 线程的 run 方法不能抛出任何被检测的异常,但是,不被检测的异常会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何 catch 子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个被用于未捕获异常的处理器。
该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有一个方法。void uncaughtException(Thread t,Throwable e)
可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。 - 线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组。
- ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法做如下操作:
1)如果该线程组有父线程组,那么父线程组的 uncaughtException 方法被调用。
2)否则,如果 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器,则调用该处理器。
3)否则,如果 Throwable 是 ThreadDeath 的一个实例,什么都不做。
4)否则,线程的名字以及 Throwable 的栈跟踪被输出到 System.err 上。 - java.lang.Thread 1.0
- static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) 5.0
- static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() 5.0
设置或获取未捕获异常的默认处理器。 - void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler) 5.0
- Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() 5.0
设置或获取未捕获异常的处理器。如果没有安装处理器,则将线程组对象作为处理器。
- java.lang.Thread.UncaughtExceptionHandler 5.0
- void uncaughtException(Thread t,Throwable e)
当一个线程因未捕获异常而终止,按规定要将客户报告记录到日志中。
参数:t 由于未捕获异常而终止的线程
e 未捕获的异常对象
- void uncaughtException(Thread t,Throwable e)
- java.lang.ThreadGroup 1.0
- void uncaughtException(Thread t,Throwable e)
如果有父线程组,调用父线程组的这一方法;或者,如果 Thread 类有默认处理器,调用该处理器,否则,输出栈踪迹到标准错误流上(但是,如果 e 是一个 ThreadDeath 对象,栈踪迹是被禁用的。 ThreadDeath 对象 stop 方法产生,而该方法已经过时)。
- void uncaughtException(Thread t,Throwable e)
14.5 同步
- 如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,这样一个情况通常称为竞争条件( race condition )。
14.5.1 竞争条件的一个例子
- 银行转账例子
14.5.2 竞争条件详解
- 一条名利是由几条指令组成的,执行它们的线程可以在任何一条指令点上被中断。
14.5.3 锁对象
- 有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。 synchronized 关键字自动提供了一个锁以及相关的“条件”,对于大多数需要显示锁的情况,这是很遍历的。 java.util.concurrent 框架为这些基础机制提供独立的类。
- 用 ReentrantLock 保护代码块的基本结构如下:
myLock.lock(); //a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock();//make sure the lock is unlocked even if an exception is three
}这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程*了锁对象,其他任何线程都无法通过 lock 语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。
- 把解锁操作放在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
- 如果使用锁,就不能使用带资源的 try 语句。首先,解锁方法名不是 close 。不过,即使将它重命名,带资源的 try 语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁,可能想使用多个线程共享的那个变量(而不是新变量)。
- 锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( hold count )来跟踪对 lock 方法的嵌套调用。线程在每一次调用lock都要调用 unlock 来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
- 通常,可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同对象。
- 要留心临界区中的代码,不要因为异常的抛出而跳出了临界区。如果在临界区代码结束之前抛出了异常, finally 子句将释放锁,但会使对象可能处于一种受损状态。
- java.util.concurrent.locks.Lock 5.0
- void lock()
获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。 - void unlock()
释放这个锁。
- void lock()
- java.util.concurrent.locks.ReentrantLock 5.0
- ReentrantLock()
构建一个可以被用来保护临界区的可重入锁。 - ReentrantLock(boolean fair)
构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是么这一公平的保证将大大降低性能。所以,默认情况下,锁没有被强制为公平的。
- ReentrantLock()
- 听起来公平锁更合理一些,但是使用公平锁比使用常规锁要慢很多。只有当你确实了解自己要做什么并且对于你要解决的问题有一个特定的理由必须使用公平锁的时候,才可以使用公平锁。即使使用公平锁,也无法确保线程调度器是公平的。如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间,那么就没有机会公平地处理这个锁了。
14.5.4 条件对象
- 通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。由于历史的原因,条件对象经常被称为条件变量( conditional variable )。
- 一个锁对象可以有一个或多个相关的条件对象。可以用 newCondition 方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它锁表达的条件的名字。如果条件不满足,调用 Condition.await() 。当前线程现在被阻塞了,并放弃了锁。
- 等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await 方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 singalAll 方法时为止。 singalAll() 调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继承执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足 -signalAll 方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。 - 通常,对 await 的调用应该在如下形式的循环体中:
while(!(ok to proceed))
condition.await(); - 至关重要的是最终需要某个其他线程调用 signalAll 方法。当一个线程调用 await 时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁( deadlock )现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法,那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
- 应该何时调用 signalAll 呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用 signalAll 。
- 注意调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
- 另一个方法 signal ,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用 signal ,那么系统就死锁了。
- 当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用 await 、 signalAll 或 signal 方法。
- java.util.concurrent.locks.Lock 5.0
- Condition newCondition()
返回一个与该锁相关的条件对象。
- Condition newCondition()
- java.util.concurrent.locks.Condition 5.0
- void await()
将该线程放到条件的等待集中。 - void signalAll()
解除该条件的等待集中的所有线程的阻塞状态。 - void signal()
从该条件的等待集中随机地选择一个线程,解除其阻塞状态。
- void await()
14.5.5 synchronized关键字
- 锁和条件的关键之处:
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码段的线程。
- 锁可以拥有一个或多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
- 从 1.0 版开始, Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
- 内部对象锁只有一个相关条件。 wait 方法添加一个线程到等待集中, notifyAll/notify 方法解除等待线程的阻塞状态。换句话说,调用 wait 或 notifyAll 等价于
intrinsicCondition.await();
intrinsicCondition.signalAll(); - wait 、 notifyAll 以及 notify 方法是 Object 类的 final 方法。 Condition 方法必须被命名为 await 、 signalAll 和 signal 以便它们不会与那些方法发生冲突。
- 将静态方法声明为 synchronized 也是合法的。如果调用这个方法,该方法获得相关的类对象的内部类。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
- 内部锁和条件存在一些局限。包括:
- 不能中断一个正在试图获得锁的线程。
- 试图获得锁时不能设定超时。
- 每个锁仅有单一的条件,可能是不够的。
- 在代码中应该使用哪一种? Lock 和 Conditon 对象还是同步方法?下面是一些建议:
- 最好既不使用 Lock/Condition 也不实用 synchronized 关键字。在许多情况下你可以使用 java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。
- 如果 synchronized 关键字适合你的程序,那么请尽量使用它。这样可以减少编写的代码数量,减少出错的几率。
- 如果特别需要 Lock/Condition 结构提供的特有特性时,才使用 Lock/Condition 。
- java.lang.Object 1.0
- void notifyAll()
解除那些在该对象上调用 wait 方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是对象锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。 - void notify()
随机选择一个在该对象上调用 wait 方法的线程,解除其阻塞状态。改方法只能在一个同步方法或同步块中调用。如果当前线程不是对象锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。 - void wait()
导致线程进入等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法抛出一个 IllegalMonitorStateException 异常。 - void wait(long millis)
- void wait(long millis,int nanos)
导致线程进入等待状态直到它被通知或者经过指定的时间。这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法抛出一个 IllegalMonitorStateException 异常。
参数:millis 毫秒数
nanos 纳秒数,<1 000 000
- void notifyAll()
14.5.6 同步阻塞
- 每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized(obj) //this is the synchronized block
{
critical section
}于是它获得 obj 的锁。
- 使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定( client-slide locking )。客户端锁定是非常脆弱的,通常不推荐使用。
14.5.7 监视器概念
- 锁和条件是线程同步的强大工具,但是,严格来讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器( monitor ),这一概念最早是由 Per Brinch Hansen 和 Tony Hoare 在 20 世纪 70 年代提出的。用 Java 的术语来讲,监视器具有如下特性:
- 监视器是只包含私有域的类。
- 每个监视器的对象有一个相关的锁。
- 使用该锁对所有的方法进行加锁。
- 该锁可以有任意多个相关条件。
- Java 设计者以不是很精确的方式采用了监视器概念, Java 中的每一个对象有一个内部的锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监视器方法。通过调用 wait/notifyAll/notify 访问条件变量。
- 在下述的 3 个方面 Java 对象不同于监视器,从而使得线程的安全性下降。
- 域不要求必须是 private 。
- 方法不要求必须是 synchronized 。
- 内部锁对客户是可用的。
14.5.8 Volatile 域
- 有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。毕竟,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大。
- 多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
- 编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变。
如果你使用锁来保护可以被多个线程访问的代码,那么可以不考虑这种问题。编译器被要求通过在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地重新排序指令。
- volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
- Volatile 变量不能提供原子性。例如,方法
private volatile boolean done;
public void flipDone(){done = !done;} //not atomic不能确保翻转域中的值。
14.5.9 final 变量
- 除非使用域或 volatile 修饰符,否则无法从多个线程安全地读取一个域。还有一种情况可以安全地访问一个共享域,即这个域声明为 final 时。考虑以下声明:
final Map<String,Double> accounts = new HashMap();
其他线程会在构造函数完成构造之后才看到这个 accounts 变量。
如果不使用 final ,就不能保证其他线程看到的是 accounts 更新后的值,它们可能都只是看到 null ,而不是新构造的 HashMap 。
对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。
14.5.10 原子性
- 假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为 volatic 。
- java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如, AtomicInteger 类提供了方法 incrementAndGet 和 decrementAndGet ,它们分别以原子方式将一个整数自增或自减。可以安全地使用 AtomicInteger 作为共享计数器而无须同步。
另外这个包中还包含 AtomicBoolean 、 AtomicLong 和 AtomicReference 以及 Boolean 值、整数、 long 值和引用的原子数组。应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。
14.5.11 死锁
- 有可能会因为每一个线程要等待条件而导致所有线程都被阻塞。这样的状态称为死锁( deadlock )。
- Java 编程语言中没有任何东西可以避免或打破这种死锁现象,必须仔细设计程序,以确保不会出现死锁。
14.5.12 线程局部变量
- 有时可能要避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。
- 要为每个线程构造一个实例,可以使用以下代码:
public static final ThreadLocal< SimpleDateFormat > dateFormat = new ThreadLocal< SimpleDateFomrat >()
{
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyy-MM-dd");
}
}要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new Date());
在一个给定线程中首次调用 get 时,会调用 initilaValue 方法。在此之后, get 方法会返回属于当前线程的那个实例。
在多个线程中生成随机数也存在类似的问题。 java.util.Random 类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,这会很低效。
可以使用 ThreadLocal 辅助类为各个线程提供一个单独的生成器,不过 Java SE 7 还另外提供一个便利类。只需要做一下调用:int random = ThreadLocalRandom.current().nextInt(upperBound);
ThreadLocalRandom.current() 调用会返回特定于当前线程的 Random 类实例。
- java.lang.ThreadLocal< T > 1.2
- T get()
得到这个线程的当前值。如果是首次调用get,会调用 initialize 来得到这个值。 - protected initialize()
应覆盖这个方法来提供一个初始值。默认情况下,这个方法返回 null 。 - void set(T t)
为这个线程设置一个新值。 - void remove()
删除对应这个线程的值。
- T get()
- java.util.concurrent.ThreadLocalRandom 7
- static ThreadLocalRandom current()
返回特定于当前线程的 Random 类实例。
- static ThreadLocalRandom current()
14.5.13 锁测试与超时
- 线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。
- TimeUnit 是一个枚举类型,可以取的值包括 SECONDS 、 MILLISECONDS 、 MICROSECONDS 和 NANOSECONDS 。
- lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么, lock 方法就无法终止。
- 然而,如果调用带有用超时参数的 tryLock ,那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。
- 也可以调用 lockInterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法。
- 在等待一个条件时,也可以提供一个超时:
myCondition.await(100,TimeUnit.MILLISECONDS)
如果一个线程被另一个线程通过调用 signalAll 或 signal 激活,或者超时时限已达到,或者线程被中断,那么 await 方法将返回。
如果等待的线程被中断, await 方法将抛出一个 InterruptedException 异常。在你希望出现这种情况时线程继续等待(可能不太合理),可以使用 awaitUniterruptibly 方法代替 await 。 - java.util.concurrent.locks.Lock 5.0
- boolean tryLock()
尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁,即使该锁有公平加锁策略,即便其他线程已经等待很久也是如此。 - boolean tryLock(long time,TimeUnit unit)
尝试获得锁,阻塞时间不会超过给定的值;如果成功返回 true 。 - void lockInterruptibly()
获得锁,但是会不确定地发生阻塞。如果线程被中断,抛出一个 InterruptedException 异常。
- boolean tryLock()
- java.util.concurrent.locks.Condition 5.0
- boolean await(long time,TimeUnit unit)
进入该条件的等待集,直到线程从等待集中移出或等待了指定的时间之后才解除阻塞。。如果因为等待时间到了而返回就返回 false ,否则返回 true 。 - void awaitUninterruptinly()
进入该条件的等待集,直到线程从等待集移出才解除阻塞。如果线程被中断,该方法不会抛出 InterruptedException 异常。
- boolean await(long time,TimeUnit unit)
14.5.14 读/写锁
- java.util.concurrent.locks包定义了两个锁类, ReentrantLock 类和 ReentrantReadWriteLock 类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者是十分有用的。在这种情况下,允许对读者共享访问是合适的。当然,写者线程依然必须是互斥访问的。
- 使用读/写锁的必要步骤:
(1)构造一个ReentrantReadWriteLock对象:private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
(2)抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();(3)对所有的获取方法加读锁:
public double getTotalBalance()
{
readLock.lock();
try{...}
finally{readLock.unlock();}
}(4)对所有的修改方法加写锁:
public void transfer(...)
{
writeLock.lock();
try{...}
finally{writeLock.unlock();}
} - java.util.concurrent.locks.ReentrantReadWriteLock 5.0
- Lock readLock()
得到一个可以被多个读操作共用的读锁,但会排斥所有写操作。 - Lock writeLock()
得到一个写锁,排斥所有其他的读操作和写操作。
- Lock readLock()
14.5.15 为什么弃用stop和suspend方法
- 初始的Java版本定义了一个 stop 方法用来终止一个线程,以及一个 suspend 方法来阻塞一个线程直至另一个线程调用 resume 。 stop 和 suspend 方法有一些共同点:都试图控制一个给定线程的行为。
这两个方法已经弃用。 stop 方法天生就不安全,经验证明 suspend 方法会经常导致死锁。 - stop 方法终止所有未结束的方法,包括 run 方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。
当线程要终止另一个线程时,无法知道什么时候调用 stop 方法是安全的,什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止。
一些作者声称 stop 方法被弃用是因为它会导致对象被一个已停止的线程永久锁定。但是,这一说法是错误的。从技术上讲,被停止的线程通过抛出 ThreadDeath 异常退出所有它所调用的同步方法。结果是,该线程释放它持有的内部对象锁。 - 与 stop 不同, suspend 不会破坏对象。但是,如果用 suspend 方法的线程试图获得同一个锁,那么,该锁在恢复之前是不可用的。如果调用 suspend 方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
- 如果想安全地挂起线程,引入一个变量 suspendRequested 并在 run 方法的某个安全的地方测试它,安全的地方是指该线程没有*其他线程需要的对象的地方。当该线程发现 suspendRequested 变量已经设置,将会保持等待状态直到它再次获得为止。
14.6 阻塞队列
- 对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。
- 当试图向队列添加元素而队列已满,或是想从队列移除元素而队列为空的时候,阻塞队列( bolcking queue )导致线程阻塞。在协调多个线程之前的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动的平衡负载。如果第一个线程集运行得比第二个慢,第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快,它将等待第二个队列集赶上来。
- 阻塞队列方法
方法 正常动作 特殊情况下的动作 add 添加一个元素 如果队列满,则抛出IllegalStateException异常 element 返回队列的头元素 如果队列空,抛出NoSuchElementException异常 offer 添加一个元素并返回true 如果队列满,返回false peek 返回队列的头元素 如果队列空,则返回null poll 移出并返回队列的头元素 如果队列空,则返回null put 添加一个元素 如果队列满,则阻塞 remove 移出并返回头元素 如果队列空,则抛出NoSuchElementException异常 take 移出并返回头元素 如果队列空,则阻塞 - 阻塞队列方法分为以下 3 类,这取决于当队列满或空时它们的响应方式。如果将队列当作线程管理工具来使用,将要用到 put 和 take 方法。当试图向满的队列中添加或从空的队列中移出元素时, add 、remove和element操作抛出异常。当然,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用offer、poll和peek方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
- poll和peek方法返回空来指示失败。因此,向这些队列中插入null值是非法的。
- 还有带有超市的offer方法和poll方法的变体。例如,下面的调用:
boolean success = q.offer(x,100,TimeUnit.MILLISECONDS);
尝试在100毫秒的时间内在队列的尾部插入一个元素。如果成功返回true;否则,达到超时时,返回false。类似地,下面的调用:
Object head = q.poll(100,TimeUnit.MILLISEDS);
尝试用100毫秒的时间移除队列的头元素;如果成功返回头元素,否则,达到在超时时,返回null。
如果队列满,则put方法阻塞;如果队列空,则take方法阻塞。在不带超时参数时,offer和poll方法等效。 - java.util.concurrent包提供了阻塞队列的几个变种。默认情况下,LinkedBlockingQueue的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque是一个双端的版本。ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,则那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。
- PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的,取元素的操作会阻塞。
- DelayQueue包含是Delayed接口的对象:
interface Delayed extends Comparable< Delayed >
{
long getDelay(TimeUnit unit);
}getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只有在延迟用完的情况下才能从DelayQueue移除。还必须实现compareTo方法。DelayQueue使用该方法对元素进行排序。
- Java SE 7增加了一个TransferQueue接口,允许生产者线程等待,直到消费者准备就绪可以接收一个元素。如果生产者调用
q.transfer(item);
这个调用会阻塞,直到另一个线程将元素(item)删除。LinkedTransferQueue实现了这个接口。
- java.util.concurrent.ArrayBlockingQueue< E > 5.0
- ArrayBlockingQueue(int capacity)
- ArrayBlockingQueue(int capacity,boolean fair)
构造一个带有指定的容量和公平性的阻塞队列。该队列用循环数组实现。
- java.util.concurrent.LinkedBlockingQueue< E > 5.0
java.uti..concurrent.LinkedBlockingDeque< E > 6.0- LinkedBlockingQueue()
- LinkedBlockingDeque()
构造一个无上限的阻塞队列或双向队列,用链表实现。 - LinkedBolckingQueue(int capacity)
- LinkedBlockingDeque(int capacity)
根据指定容量构建一个有限的阻塞队列或双向队列,用链表实现。
- java.util.concurrent.DelayQueue< E extends Delayed > 5.0
- DelayQueue()
构造一个包含Delayed元素的*的阻塞时间有限的阻塞队列。只有那些延迟已经超过时间的元素可以从队列中移出。
- DelayQueue()
- java.util.concurrent.Delayed 5.0
- long getDelay(TimeUnit unit)
得到该对象的延迟,用给定的时间单位进行度量。
- long getDelay(TimeUnit unit)
- java.util.concurrent.PriorityBlockingQueue< E > 5.0
- PriorityBlockingQueue()
- PriorityBlockingQueue(int initialCapacity)
- PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator)
构造一个无边界阻塞优先队列,用堆实现。
参数:initialCapacity 优先队列的初始容量。默认值是11。
comparator 用来对元素进行比较的比较器,如果没有指定,则元素必须实现Comparable接口。
- java.util.concurrent.BlockingQueue< E > 5.0
- void put(E element)
添加元素,在必要时阻塞。 - E take()
移除并返回头元素,必要时阻塞。 - boolean offer(E element,long time,TimeUnit unit)
添加给定的元素,如果成功返回true,如果必要时阻塞,直至元素已经被添加或超时。 - E poll(long time,TimeUnit unit)
移除并返回头元素,必要时阻塞,直至元素可用或超时用完。失败时返回null。
- void put(E element)
- java.util.concurrent.BolckingDeque< E > 6
- void putFirst(E element)
- void putLast(E element)
添加元素,必要时阻塞。 - E takeFirst()
- E takeLast()
移除并但会头元素或尾元素,必要时阻塞。 - boolean offerFirst(E element,long time,TimeUnit unit)
- boolean offerLast(E element,long time,TimeUnit unit)
添加给定的元素,成功时返回true,必要时阻塞直至元素被添加或超时。 - E pollFirst(long time,TimeUnit unit)
- E pollLast(long time,TimeUnit unit)
移动并返回头元素或尾元素,必要时阻塞,直至元素可用或超时。失败时返回null。
- java.util.concurrent.TransferQueue< E > 7
- void transfer(E element)
- boolean tryTransfer(E element,long time,TimeUnit unit)
传输一个值,或者尝试在给定的超时时间内传输这个值,这个调用将阻塞,直到另一个线程将元素删除。第二个方法会在调用成功时返回true。
14.7 线程安全的集合
- 可以通过提供锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易写。
14.7.1 高效的映射表、集合和队列
- java.util.concurrent包提供了映射表、有序集合队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。
- 与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。
- 集合返回弱一致性(weakly consisteut)的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会抛出Concurrent ModificationException异常。
- 与之形成对照的是,集合如果在迭代器构造之后发生改变,java.util包中的迭代器将抛出一个ConcurrentModificationException异常。
- 并发地散列映射表,可高效地支持大量的读者和一定数量的写者。默认情况下,假定可以有多达16个写者线程同时执行。可以有更多的写者线程,但是,如果同一时间多于16个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而,没有这种必要。
- ConcurrentHashMap和ConcurrentSkipListMap类有相应的方法用于原子性的关联插入以及关联删除。putIfAbsent方法自动地添加新的关联,前提是原来没有这一关联。对于多线程访问的缓存来说这是很有用的,确保只有一个线程向缓存添加项:
cache.putIfAbsent(key,value);
相反的操作是删除(或许应该叫做removeIfPresent)。调用
cache.remove(key,value);
将原子性地删除键值对,如果它们在映像表中出现的话。最后,
cache.replace(key,oldValue,newValue);
原子性地用新值替换旧值,假定旧值与指定的键值关联。
- java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
- ConcurrentLinkedQueue< E >()
构造一个可以被多线程安全访问的无边界非阻塞的队列。
- ConcurrentLinkedQueue< E >()
- java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
- ConcurrentSkipListSet< E >()
- ConcurrentSkipListSet< E >(Comparator<? super E> comp)
构造一个可以被多线程安全访问的有序集。第一个构造器要求元素实现Comparable接口。
- java.util.concurrent.ConcurrentHashMap< K,V > 5.0
java.util.concurrent.ConcurrentSkipListMap< K,V > 6- ConcurrentHashMap< K,V >()
- ConcurrentHashMap< K,V >(int initialCapacity)
- ConcurrentHashMap< K,V >(int initialCapacity,float loadFactor,int concurrencyLevel)
构造一个可以被多线程安全访问的散列映射表。
参数:initialCapacity 集合的初始容量。默认值为16。
loadFactor 控制调整:如果每一个桶的平均负载超过这个因子,表的大小会被重新调整。默认值是0.75。
concurrencyLevel 并发写者线程的估计数目。 - ConcurrentSkipListMap< K,V >()
- ConcurrentSkipListSet< K,V >(Comparator<? super K> comp)
构造一个可以被多线程安全访问的有序的映像表。第一个构造器要求键实现Comparable接口。 - V putIfAbsent(K key,V value)
如果该键没有在映像表中出现,则将给定的值同给定的键关联起来,并返回null。否则返回与该键关联的现有值。 - boolean remove(K hey,V value)
如果给定的键与给定的值关联,删除给定的键与值并返回真。否则,返回false。 - boolean replace(K key,V oldValue,V newValue)
如果给定的键当前与oldvalue相关联,用它与newValue关联。否则,返回false。
14.7.2 写数组的拷贝
- CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数,这样的安排是很有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组,但是,集合的数组已经被替换了。因此,旧的迭代器拥有一致的(可能过时的)视图,访问它无须同步开销。
14.7.3 较早的线程安全集合
- 从Java的初始版本开始,Vector和Hashtable类就提供了线程安全的动态数组和散列表的实现。现在这些类被弃用了,取而代之的是ArrayList和HashMap类。这些类不是线程安全的。而集合库中提供了不同的机制。任何集合类通过使用同步包装器(synchronization wrapper)变成线程安全的:
List< E > synchArrayList = Collections.synchronizedList(new ArrayList< E >());
Map< K,V > synchHashMap = Collections.synchronizedMap(new HashMap< K,V >()); - 结果集合的方法使用锁加以保护,提供了线程的安全访问。
- 应该确保没有任何线程通过原始的非同步方法访问数据结构。最便利的方法是确保不保存任何指向原始对象的引用,简单地构造一个集合并立即传递给包装器。
- 如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“客户端”锁定:
synchronized(synchHashMap)
{
Iterator< K > iter = synchHashMap.keySet().iterator();
while(iter.hashNext())...;
} - 如果使用“for each”循环必须使用同样的代码,因为循环使用了迭代器。注意:如果在迭代过程中,别的线程修改集合,迭代器会失效,抛出ConcurrentModificationException异常。同步仍然是需要的,因此并发的修改可以被可靠地检测出来。
- 最好使用java.util.concurrent包中的集合,不使用同步包装器中的。特别是,加入它们访问的是不同的桶,由于ConcurrentHashMap已经精心地实现了,多线程可以访问它而且不会彼此阻塞。有一个例外是经常被修改的数组列表。在那种情况下,同步的ArrayList可以胜过CopyOnWriteArrayList。
- java.util.collections 1.2
- static < E > Collection< E > synchronizedCollection(Collection< E > c)
- static < E > List synchronizedList(List< E > c)
- static < E > Set synchronizedSet(Set< E > c)
- static < E > SortedSet synchronizedSortedSet(SortedSet< E > c)
- static < K,V > Map< K,V > synchronizedMap(Map< K,V > c)
- static < K,V > SortedMap< K,V > synchronizedSortedMap(SortedMap< K,V > c)
构造集合视图,该集合的方法是同步的。
14.8 Callable与Future
- Runnable封装一个异步运行的任务,可以把它想想成为一个没有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call。
public interface Callable< V >
{
V call() throws Exception;
}类型参数是返回值的类型。例如,Callable< Integer >表示一个最终返回Integer对象的异步计算。
- Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在结果计算好之后就可以获得它。
- Future接口具有下面的方法:
public interface Future< V >
{
V get() thros ...;
V get(long timeout,TimeUnit unit) throwa...;
void cancel(boolean mayInterupt);
boolean isCancelled();
boolean isDone();
}第一个get方法的调用被阻塞,直到计算完成。如果在计算完成之前,第二个方法的调用超时,抛出一个TimeoutException异常。如果运行该计算的线程被中断,两个方法都将抛出InterruptedException。如果计算已经完成,那么get方法立即返回。
如果计算还在进行,isDone方法返回false;如果完成了,则返回true。
可以用cancel方法取消该计算。如果计算还没有开始,它被取消且不再开始。如果计算处于运行之中,那么如果mayInterrupt参数为true,它就被中断。 - FutureTask包装器是一种非常便利的机制,可将Callable转换成Future和Runnable,它同时实现二者的接口。
- java.util.concurrent.Callable< V > 5.0
- V call()
运行一个将产生结果的任务。
- V call()
- java.util.concurrent.Future< V > 5.0
- V get()
- V get(long time,TimeUnit unit)
获取结果,如果没有结果可用,则阻塞直到得到结果超过指定的事件为止。如果不成功,第二个方法会抛出TimeoutException异常。 - boolean cancel(boolean mayInterrupt)
尝试取消这一任务的运行。如果任务已经开始,并且mayInterrupt参数值为true,它就会被中断。如果成功执行了取消操作,返回true。 - boolean isCancelled()
如果任务在完成前被取消了,则返回true。 - boolean isDone()
如果任务结束,无论是正常结束、中途取消或发生异常,都返回true。
- java.util.concurrent.FutureTask< V > 5.0
- FutureTask(Callable< V > task)
- FutureTask(Runnable task,V result)
构造一个既是Future< V >又是Runnable的对象。
14.9 执行器
- 构造一个新的线程是由一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命周期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
- 另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用线程数“固定的”线程池以限制并发线程的总数。
- 执行器(Executor)类有许多静态工厂方法用来构建线程池。
- 执行者工厂方法
方法 描述 newCachedThreadPool 必要时创建新线程;空闲线程会被保留60秒 newFixedThreadPool 该池包含固定数量的线程;空闲线程会一直被保留 newSingleThreadExecutor 只有一个线程的“池”,该线程顺序执行每一个提交的任务(类似于Swing事件分配线程) newScheduledThreadPool 用于预定执行而构建的固定线程池,替代java.util.Timer newSingleThreadScheduleExecutor 用于预定执行而构建的单线程“池”
14.9.1 线程池
- newCachedThreadPool方法构建了一个线程池。对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则构建一个新线程。newFixedThreadPool方法构建一个具有固定大小的线程池。如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中。当其他任务完成以后再运行它们。newSingleThreadExecutor是一个退化了的大小为1的线程池:由一个线程执行提交的任务,一个接着一个。这3个方法返回实现了ExecutorService接口的ThreadPoolExecutor类的对象。
- 可用下面的方法之一将一个Runnable对象或Callable对象提交给ExecutorService:
Future<?> submit(Runnable task)
Future< T > submit(Runnable task,T result)
Future< T > submit(Callable< T > task)该池会在方便的时候尽早执行提交的任务。调用submit时,会得到一个Future对象,可用来查询该任务的状态。
第一个submit方法返回一个奇怪样子的Future<?>。可以使用这样一个对象来调用isDone、cancel或isCancelled。但是,get方法在完成的时候只是简单地返回null。
第二个版本的Submit也提交一个Runnable,并且Future的get方法在完成的时候返回指定的result对象。
第三个版本的Submit提交一个Callable,并且返回的Future对象将在计算结果准备好的时候得到它。 - 当用完一个线程池的时候,调用shutdown。该方法启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。另一个方法是调用shutdownNow。该池取消尚未开始的所有任务并试图中断正在运行的线程。
- 下面总结了在使用连接池时应该做的事:
(1)调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool。
(2)调用submit提交Runnable或Callable对象。
(3)如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象。
(4)当不再提交任何任务时,调用shutdown。 - java.util.concurrent.Executors 5.0
- ExecutorService newCachedThreadPool()
返回一个带缓存的线程池,该池在必要的时候创建线程,在线程空闲60秒之后终止线程。 - ExecutorService newFixedThreadPool(int threads)
返回一个线程池,该池中的线程数由参数指定。 - ExecutorService newSingleThreadExecutor()
返回一个执行器,它在一个单个的线程中一次执行各个任务。
- ExecutorService newCachedThreadPool()
- java.util.concurrent.ExecutorService 5.0
- Future< T > submit(Callable< T > task)
- Future< T > submit(Runnable task,T result)
- Future< ? > submit(Runnable task)
提交指定的任务去执行。 - void shutdown()
关闭服务,会先按成已经提交的任务而不再接收新的任务。
- java.util.concurrent.ThreadPoolExecutor 5.0
- int getLargestPoolSize()
返回线程池在该执行器盛行周期中的最大尺寸。
- int getLargestPoolSize()
14.9.2 预定执行
- ScheduleExecutorService接口具有为预定执行(Scheduled Execution)或重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛化。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象。
- 可以预定Runnable或Callable在初始的延迟之后只运行一次。也可以预定一个Runnable对象周期性地运行。
- java.util.concurrent.Executors 5.0
- ScheduledExcutorService newScheduledThreadPool(int threads)
返回一个线程池,它使用给定的线程数来调度任务。 - ScheduledExecutorService newSingleThreadScheduledExecutor()
返回一个执行器,它在一个单独线程中调度任务。
- ScheduledExcutorService newScheduledThreadPool(int threads)
- java.util.concurrent.ScheduledExecutorService 5.0
- ScheduledFuture< V > schedule(Callable< V > task,long time,TimeUnit unit)
- ScheduledFuture< ? > schedule(Runnable task,long time,TimeUnit unit)
预定在指定的时间之后执行任务。 - ScheduledFuture<?> scheduleAtFixedRate(Runnable task,long initialDelay,long period,TimeUnit unit)
预定在初始的延迟结束后,周期性地运行给定的任务,周期长度是period。 - ScheduledFuture<?> scheduleWithFixedDelay(Runnable task,long initialDelay,long delay,TimeUnit unit)
预定在初始的延迟结束后周期性地给定的任务,再一次调用完成和下一次调用开始之间有长度为delay的延迟。
14.9.3 控制任务组
- invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。无法知道返回的究竟是哪个任务的结果,也许是最先完成的那个任务的结果。对于搜素问题,如果你愿意接受任何一种解决方案的话,就可以使用这个方法。
- invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可以像下面这样对结果进行处理:
List<Callable< T >> tasks=...;
List<Future< T >> results = executor.invokeAll(tasks);
for (Future< T > result:results)
processFurther(result.get());这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能按可获得的顺序保存起来更有实际意义。可以用ExecutorCompletionService来进行排列。
- 用常规的方法获得一个执行器。然后,构建一个ExecutorCompletionService,提交任务给完成服务(completion service)。该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果(当这些结果成为可用时)。这样一来,相比前面的计算,一个更有效的组织形式如下:
ExecutorCompletionService service = new ExecutorCompletionService(executor);
for(Callable< T > task:tasks) service.submit(task);
for (int i=0;i<task.size();i++)
processFurther(service.task().get()); - java.util.concurrent.ExecutorService 5.0
- T invokeAny(Collection< Callable< T >> tasks)
- T invokeAny(Collection< Callable< T >> tasks,long timeout,TimeUnit unit)
执行给定的任务,返回其中一个任务的结果。第二个方法若发生超时,抛出一个Timeout Exception异常。 - List< Future< T >> invokeAll(Collection<callable< t="">> tasks)
- List< Future< T >> invokeAll(Collection< Callable< T >> tasks,long timeout,TimeUnit unit)
执行给定的任务,返回所有执行的结果。第二个方法若发生超时,抛出一个TimeOutEException超时。
- java.util.concurrent.ExecutorCompletionService 5.0
- ExecutorCompletionService(Executor e)
构造一个执行器完成服务来收集给定执行器的结果。 - Future< T > submit(Callable< T > task)
- Future< T > submit(Runnable task,T result)
提交一个任务给底层的执行器。 - Future< T > task()
移除下一个已完成的结果,如果没有任何已完成的结果可用则阻塞。 - Future< T > poll()
- Future< T > poll(long time,TimeUnit unit)
移除下一个已完成的结果,如果没有任何已完成结果可用则返回null。第二个方法将等待给定的时间。
- ExecutorCompletionService(Executor e)
14.9.4 Fork-Join框架
- 有些应用使用了大量线程,但其中大多数都是空闲的。举例来说,一个Web服务器可能会为每个连接分别使用一个线程。另外一些应用可能对每个处理器内核分别使用一个线程,来完成计算密集型任务,如图像或视频处理。Java SE 7中新引入了fork-join框架,专门用来支持后一类应用。假设有一个处理任务,它可以很自然地分解为子任务,如下所示:
if(problemSize > threshold)
solve problem direckly
else
{
break problem into subproblems
recursively solve each subproblem
combine the results
} - 在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方法称为工作密取(work stealing)。每个工作线程都有一个双端队列(deque)来完成任务。一个工作线程将子任务压入其双端队列的队头。(只有一个线程可以访问队头,所以不需要加锁。)一个工作线程空闲时,它会从另一个双端队列的队尾“密取”一个任务。由于大的子任务都在队尾,这种密取很少出现。
14.10 同步器
- java.util.concurrent包包含了几个能帮助人们管理相互合作的线程集的类见下表。这些机制具有为线程之间的共用集结点模式(common rendezous patterns)提供的“预置功能”(canned functionality)。如果有一个相互合作的线程集满足这些行为模式之一,那么应该直接重用合适的库类而不要试图提供手工的锁与条件的集合。
同步器类 它能做什么 何时使用 CyclicBarrier 允许线程集等待直至其中预定数目的线程到达一个公共障栅(barrier),然后可以选择执行一个处理障栅的动作 当大量的线程需要在它们的结果可用之前完成时 CountDownLatch 允许线程集等待直到计数器减为0 当一个或多个线程需要等待直到指定数目的事件发生 Exchanger 允许两个线程在要交换的对象准备好时交换对象 当两个线程工作在同一个数据结构的两个实例上的时候,一个向实例添加数据而另一个从实例清除数据 Semaphore 允许线程集等待直到被允许继续运行为止 限制访问资源的线程总数。如果许可数是1,常常阻塞线程直到另一个线程给出许可为止 SynchronoutQueue 允许一个线程把对象交给另一个线程 在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时
14.10.1 信号量
- 概念上讲,一个信号量管理许多的许可证( permits )。为了通过信号量,线程通过调用 acquire 请求许可。其实没有实际的许可对象,信号量仅维护一个计数。许可的数目是固定的,由此限制了通过的线程数量。其他线程可以通过调用 release 释放许可。而且,许可不是必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。
- 信号量在 1968 年由 Edsger Dijkstra 发明,作为同步原语( synchronization primitive )。 Dijkstra指出信号量可以被有效地实现,并且有足够的能力解决许多常见的线程同步问题。在几乎任何一本操作系统教科书中,都能看到使用信号量实现的有界队列。
应用程序员不必自己实现有界队列。通常,信号量不必直接映射到通用应用场景。
14.10.2 倒计时门栓
- 一个倒计时门栓( CountDownLatch )让一个线程集等待直到计数变为 0 。倒计时门栓是一次性的。一旦计数为 0 ,就不能再重用了。
- 一个有用的特例是计数值为 1 的门栓。实现一个只能通过一次的门。线程在门外等候直到另一个线程将计数器值置为 0 。
14.10.3 障栅
- CyclicBarrier 类实现了一个集结点( rendezvous )称为障栅( barrier )。考虑大量线程运行在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
- 如果任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可能离开是因为它调用 await 时设置了超时,或者因为它被中断了)。在这种情况下,所有其他线程的 await 方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await 的调用。
- 障栅被称为是循环的( cyclic ),因为可以在所有等待线程被释放后被重用。在这一点上,有别于 CountDownLatch , CountDownLatch 只能被运行一次。
- Phaser 类增加了更大的灵活性,云溪改变不同阶段中参与线程的个数。
14.10.4 交换器
- 当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用变换器( Exchanger )。典型的情况是,一个线程想缓冲区填入数据,另一个线程消耗这些数据。当它们都完成以后,相互交换缓冲区。
14.10.5 同步队列
- 同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另一个线程调用 take 方法为止,反之亦然。与 Exchanger 的情况不同,数据仅仅沿一个方向传递,从生产者到消费者。
- 即使 SynchronousQueue 类实现了 BlockingQueue 接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的 size 方法总是返回 0 。
14.11 线程与Swing
- 在程序中使用线程的理由之一是提高程序的响应性能。当程序需要做某些耗时的工作时,应该启动另一个工作器线程而不是阻塞用户接口。
- 必须认真考虑工作器线程在做什么,因为这或许令人惊讶, Swing 不是线程安全的。如果试图在多个县城中操纵用户界面的元素,那么用户界面可能奔溃。
14.11.1 运行耗时的任务
-
将线程与 Swing 一起使用时,必须遵循两个简单的原则。
- 如果一个动作需要花费很长时间,在一个独立的工作器线程中做这件事不要在事件分配线程中做。
- 除了事件分配线程,不要在任何线程中接触 Swing 组件。
制定第一条规则的理由易于理解。如果花很多时间在事件分配线程上,应用程序像“死了”一样,因为它不响应任何事件。特别是,事件分配线程应该永远不要进行 input/output 调用,这有可能会阻塞,并且永远不要调用 sleep 。(如果需要等待指定的时间,使用定时器事件。)
第二条规则在 Swing 编程中通常称为单一线程规则( single-thread rule )。
这两条规则看起来彼此冲突。假定要启动一个独立的线程运行一个耗时的任务。线程工作的时候,通常要灯芯用户界面中指示执行的进度。任务完成的时候,要再一次更新 GUI 界面。但是,不能从自己的线程接触 Swing 组件。例如,如果要更新进度条或标签文本,不能从线程中设置它的值。
要解决这一问题,在任何线程中,可以使用两种有效的方法向事件队列添加任意的动作。例如,假定想在一个线程中周期性地更新标签来表明进度。不可以从自己的线程中调用 label.setText ,而应该使用 EventQueue 类的 invokeLater 方法和 invokeAndWait 方法使所调用的方法在事件分配线程中执行。
应该将 Swing 代码放置到实现 Runnable 接口的类的 run 方法中。然后,创建该类的一个对象,将其传递给静态的 invokeLater 或 invokeAndWait 方法。
当事件放入事件队列时, invokeLater 方法立即返回,而 run 方法被异步执行。 invokeAndWait 方法等待直到 run 方法确实被执行过为止。
有更新进度标签时, invokeLater 方法更适宜。用户更希望让工作器线程有更快完成工作而不是得到更加精确的进度指示器。
这两种方法都是在事件分配线程中执行 run 方法。没有新的线程被创建。 - java.awt.EventQueue 1.1
- static void invokeLater(Runnable runnable) 1.2
在待处理的线程被处理之后,让 runnable 对象的 run 方法在事件分配线程中执行。 - static void invokeAndWait(Runnable runnable) 1.2
在待处理的线程被处理之后,让 runnable 对象的 run 方法在事件分配线程中执行。该调用会阻塞,直到 run 方法终止。 - static boolean isDispatchThread() 1.2
如果执行这一方法的线程是事件分配线程,返回 true。
- static void invokeLater(Runnable runnable) 1.2
14.11.2 使用Swing工作线程
- SwingWorker 类覆盖 doInBackground 方法来完成耗时的工作,调用 publish 来报告工作进度。这一方法在工作器线程中执行。 publish 方法使得 process 方法在事件分配线程中执行来处理进度数据。当工作完成时, done 方法在事件分配线程中被调用以便完成 UI 的更新。
每当要在工作器线程中做一些工作时,构建一个新的工作器(每一个工作器对象只能被使用一次)。然后调用 execute 方法。典型的方式是在事件分配线程中调用 execute ,但没有这样的需求。 - SwingWorker 类有 3 种类型作为类型参数。 SwingWorker< T,V > 产生类型为 T 的结果以及类型为 V 的进度数据。
- 要取消正在进行的工作,使用 Future 接口的 cancel 方法。当该工作被取消的时候, get 方法抛出 CancellationException 异常。
- 工作器线程对 publish 的调用会导致在事件分配线程上的 process 的调用。为了提高效率,几个对 publish 的调用结果,可用对 process 的一次调用成批处理。 process 方法接收一个包含所有中间结果的列表< V > 。
- javax.swing.SwingWorker< T,V > 6
- abstract T doInBackground()
覆盖这一方法来执行后台的任务并返回这一工作的结果。 - void process(Lisy< V > data)
覆盖这一方法来处理事件分配线程中的中间进度数据。 - void publish(V… data)
传递中间进度数据到事件分配线程。从 doInBackground 调用这一方法。 - void execute()
为工作器线程的执行预定这个工作器。 - SwingWorker.StateValue getState()
得到这个工作器线程的状态,值为 PENDING 、 STARTED 或 DONE 之一。
- abstract T doInBackground()
14.11.3 单一线程规则
- 单一线程规则:“除了事件分配线程,不要在任何线程中接触 Swing 组件”。
- 对于单一线程规则存在一些例外情况:
- 可在任一个线程里添加或移除事件监听器。当然该监听器的方法会在事件分配线程中被触发。
- 只有很少的 Swing 方法是线程安全的。在 API 文档中用这样的句子特别标明: “尽管大多数 Swing 方法不是线程安全的,但这个方法是。”在这些线程安全的方法中最有用的是:
JTextComponent.setText
JtextArea.insert
JTextArea.append
JTextArea.replaceRange
JComponent.repaint
JComponent.revalidate
- 历史上,单一线程规则是更加随意的。任何线程都可以构建组件,设置优先级,将它们添加到容器中,只要这些组件没有一个是已经被实现的( realized )。如果组件可以接受 paint 事件或 validation 事件,组件被实现。一旦调用组件的 setVisible(true) 或 pack(!) 方法或者组件已经被添加到已经被实现的容器中,就出现这样的情况。