Java核心技术卷一基础知识-第14章-多线程-读书笔记

第 14 章 多线程

本章内容:
* 什么是线程
* 中断线程
* 线程状态
* 线程属性
* 同步
* 阻塞队列
* 线程安全的集合
* Collable与Future
* 执行器
* 同步器
* 线程与Swing
  1. 一般,每个任务称为一个线程(thread),它是线程控制的简称。能够同时运行一个以上线程的程序称为多线程程序(multithreaded)。
  2. 多进程与多线程有哪些区别呢?本质的区别在于每一个进程拥有本身的一整套变量,而线程则共享数据。共享变量使线程之间的通讯比进程之间的通讯更有效、更容易。此外,在有些操做系统中,与进程相比较,线程更“轻量级”,建立、撤销一个线程比启动新进程的开销要小得多。

14.1 什么是线程

  1. Thread 类的静态 sleep 方法将暂停给定的毫秒数。调用 Thread.sleep 不会建立一个新线程, sleep 是 Thread 类的静态方法,用于暂停当前线程的活动。 sleep 方法能够抛出一个 InterruptedException 异常。
  2. java.lang.Thread 1.0
    • static void sleep(long millis)
      休眠给定的毫秒数。
      参数:millis 休眠的毫秒数
  3. 不要调用 Thread 类或 Runnable 对象的 run 方法。直接调用 run 方法,只会执行同一个线程中的任务,并不会启动新线程。应该调用 Thread.start 方法。这个方法将建立一个执行 run 方法的新线程。
  4. java.lang.Thread 1.0
    • Thread(Runnable target)
      构造一个新线程,用于调用给定target的run()方法。
    • void start()
      启动这个线程,将引起调用run()方法。这个方法将当即返回,而且新线程将并行运行。
    • void run()
      调用关联Runnable的run方法。
  5. java.lang.Runnable 1.0
    • void run()
      必须覆盖这个方法,并在这个方法中提供所要执行的任务指令。

14.2 中断线程

  1. 当线程的 run 方法执行方法体重最后一条语句后,并经由执行 return 语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。在Java的早期版本中,还有一个 stop 方法,其余线程能够调用它终止线程。可是,这个方法如今已经被弃用了。
  2. 有一种能够强制线程终止的方法。然而, interrupt 方法能够用来请求终止线程。
  3. 当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。这是每个线程都具备的 boolean 标志。每一个线程都应该不时地检查这个标志,以判断线程是否被中断。
  4. 调用 Thread.currentThread().isInterrputed() 方法得到当前线程的中断状态是否被置位。可是,若是线程被阻塞,就没法检测中断状态。这是产生 InterruptedException 异常的地方。当在一个被阻塞的线程(调用 sleep 或 wait )上调用 interrupt 方法时,阻塞调用将会被 Interrupt Exception 异常中断(存在不能被中断的阻塞 I/O 调用,应该考虑选择可中断的调用)。
  5. 没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引发它的注意。被中断的线程能够决定如何响应中断。某些线程是如此重要以致于应该处理完异常后,继续执行,而不理会中断。可是,更普通的状况是,线程将简单地将中断做为一个终止的请求。
  6. 若是在每次工做迭代以后都调用 sleep 方法(或者其余的可中断方法), isInterrpted 检测既没有必要也没有用处。若是在种蒜状态被置位时调用 sleep 方法,它不会休眠。相反,它将清除这一状态(!)并抛出 InterrputedException 。所以,若是你的循环调用 sleep ,不会检测中断状态,相反,须要捕获 InterrputedException 异常。
  7. 有两个很是相似的方法, interrupted 和 isInterrupted 。 Interrupted 方法是一个静态方法,它检测当前的线程是否被中断。并且,调用 interrupted 方法会清除该线程的中断状态。另外一方面, isInterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。
  8. java.lang.Thread 1.0
    • void interrupt()
      向线程发送中断请求。线程的中断状态将设置为 true 。若是目前该线程被一个 sleep 调用阻塞,那么, InterruptedException 异常被抛出。
    • static boolean interrupted()
      测试当前线程(即正在执行这一命令的线程)是否被中断。注意,这是一个静态方法。这一调用会产生反作用-它将当前线程的中断状态重置为false。
    • boolean isInterrupted()
      测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
    • static Thread currentThread()
      返回表明当前执行线程的 Thread 对象。

14.3 线程状态

  1. 线程能够有以下6种状态:
    • New (新建立)
    • Runnable (可运行)
    • Blocked (被阻塞)
    • Waiting (等待)
    • Timed waiting (计时等待)
    • Terminated (被终止)

14.3.1 新建立线程

  1. 当用new操做符建立一个新线程,如 new Thread(r) ,该线程尚未开始运行。这意味着它的状态是 new 。当一个线程处于新建立状态时,程序尚未开始运行线程中的代码。在线程运行以前还有一些基本工做要作。

14.3.2 可运行线程

  1. 一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操做系统给线程提供运行的时间。( Java 的规范说明没有将它做为一个单独状态。一个正在运行中的线程仍然处于可运行状态。)
  2. 一旦一个线程开始运行,它没必要始终保持运行。事实上,运行中的线程被中断,目的是为了让其余线程得到运行机会。线程调度的细节依赖于操做系统提供的服务。抢占式调度系统给每个可运行线程一个时间片来执行任务。当时间片用完,操做系统剥夺该线程的运行权,并给另外一个线程运行机会。当选择下一个线程时,操做系统考虑线程的优先级。
  3. 在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为何将这个状态称为可运行而不是运行)。

14.3.3 被阻塞线程和等待线程

  1. 当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少资源。直到线程调度器从新激活它。细节取决于它是怎样达到非活动状态的。
    • 当一个线程试图获取一个内部的对象锁(而不是 java.util.concurrect 库中的锁),而该锁被其余线程持有,则该线程进入阻塞状态。当全部其余线程释放该锁,而且线程调度器容许本线程持有它的时候,该线程将变成非阻塞状态。
    • 当线程等待另外一个线程通知调度器一个条件时,它本身进入等待状态。在调用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 库中的 Lock 或 Condition 时,就会出现这种状况。实际上,被阻塞状态与等待状态是由很大不一样的。
    • 有几个方法有一个超时参数。调用它们致使线程进入计时等待( timed waiting )状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.wait 、 Thread.join 、 Lock.tryLock 以及 Condition.awit 的计时版。
  2. 当一个线程被阻塞或等待时(或终止时),另外一个线程被调度为运行状态。当一个线程被从新激活(例如,由于超时期满或成功地得到一个锁),调度器检查它是否具备比当前运行线程更高的优先级。若是是这样,调度器从当前运行线程中挑选一个,剥夺其运行权,选择一个新的线程运行。
  3. 线程状态图

14.3.4 被终止的线程

  1. 线程因以下两个缘由之一而被终止:
    • 由于 run 方法正常退出而天然死亡。
    • 由于一个没有捕获的异常终止了 run 方法而意外死亡。
      特别是,能够调用线程的 stop 方法杀死一个线程。该方法抛出 ThreadDeath 错误对象,由此杀死线程。可是, stop 方法已过期,不要在本身的代码中调用这个方法。
  2. 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()以后调用。这一方法已过期。

14.4 线程属性

  1. 线程的各类属性,其中包括:线程优先级、守护线程、线程组以及处理未捕获异常的处理器。

14.4.1 线程优先级

  1. 在 Java 程序设计语言中,每个线程有一个优先级。默认状况下,一个线程继承它的父线程的优先级。能够用 setPriority 方法提升或下降任何一个线程的优先级。能够将优先级设置为在 MIN_PRIORITY (在 Thread 类中定义为 1 )与 MAX_PRIORITY (定义为 10 )之间的任何值。 NORM_PRIORITY 被定义为 5 。
  2. 每当线程调度器有机会选择新线程时,它首先选择具备较高优先级的线程。可是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时, Java 线程的优先级被映射到宿主主机平台的优先级上,优先级个数也许更多,也许更少。
    Windows 有 7 个优先级别。一些 Java 优先级将映射到相同的操做系统优先级。在 Sun 为 Linux 提供的 Java 虚拟机,线程的优先级被忽略-全部线程具备相同的优先级。
  3. 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()
      致使当前执行线程处于让步状态。若是有其余的可运行线程具备至少与此线程一样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。

14.4.2 守护线程

  1. 能够经过 t.setDaemon(true) 将线程转换为守护线程( daemon thread )。守护线程的惟一用途是为其余线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其余线程或清空过期的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,因为若是只剩下守护线程,就不必继续运行程序了。
  2. 守护线程应该永远不去访问固有资源,如文件、数据库,由于它会在任什么时候候甚至在一个操做的中间发生中断。
  3. java.lang.Threed 1.0
    • void setDaemon(boolean isDaemon)
      标识该线程为守护线程或用户线程。这一方法必须在线程启动以前调用。

14.4.3 未捕获异常处理器

  1. 线程的 run 方法不能抛出任何被检测的异常,可是,不被检测的异常会致使线程终止。在这种状况下,线程就死亡了。
    可是,不须要任何 catch 子句来处理能够被传播的异常。相反,就在线程死亡以前,异常被传递到一个被用于未捕获异常的处理器。
    该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有一个方法。
    void uncaughtException(Thread t,Throwable e)
    能够用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也能够用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为全部线程安装一个默认的处理器。替换处理器可使用日志API发送未捕获异常的报告到日志文件。
    若是不安装默认的处理器,默认的处理器为空。可是,若是不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。
  2. 线程组是一个能够统一管理的线程集合。默认状况下,建立的全部线程属于相同的线程组,可是,也可能会创建其余的组。如今引入了更好的特性用于线程集合的操做,因此建议不要在本身的程序中使用线程组。
  3. ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法作以下操做:
    1)若是该线程组有父线程组,那么父线程组的 uncaughtException 方法被调用。
    2)不然,若是 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器,则调用该处理器。
    3)不然,若是 Throwable 是 ThreadDeath 的一个实例,什么都不作。
    4)不然,线程的名字以及 Throwable 的栈跟踪被输出到 System.err 上。
  4. 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
      设置或获取未捕获异常的处理器。若是没有安装处理器,则将线程组对象做为处理器。
  5. java.lang.Thread.UncaughtExceptionHandler 5.0
    • void uncaughtException(Thread t,Throwable e)
      当一个线程因未捕获异常而终止,按规定要将客户报告记录到日志中。
      参数:t 因为未捕获异常而终止的线程
      e 未捕获的异常对象
  6. java.lang.ThreadGroup 1.0
    • void uncaughtException(Thread t,Throwable e)
      若是有父线程组,调用父线程组的这一方法;或者,若是 Thread 类有默认处理器,调用该处理器,不然,输出栈踪影到标准错误流上(可是,若是 e 是一个 ThreadDeath 对象,栈踪影是被禁用的。 ThreadDeath 对象 stop 方法产生,而该方法已通过时)。

14.5 同步

  1. 若是两个线程存取相同的对象,而且每个线程都调用了一个修改该对象状态的方法,这样一个状况一般称为竞争条件( race condition )。

14.5.1 竞争条件的一个例子

  1. 银行转帐例子

14.5.2 竞争条件详解

  1. 一条名利是由几条指令组成的,执行它们的线程能够在任何一条指令点上被中断。

14.5.3 锁对象

  1. 有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,而且 Java SE 5.0 引入了 ReentrantLock 类。 synchronized 关键字自动提供了一个锁以及相关的“条件”,对于大多数须要显示锁的状况,这是很遍历的。 java.util.concurrent 框架为这些基础机制提供独立的类。
  2. 用 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 时,它们被阻塞,直到第一个线程释放锁对象。
  3. 把解锁操做放在 finally 子句以内是相当重要的。若是在临界区的代码抛出异常,锁必须被释放。不然,其余线程将永远阻塞。
  4. 若是使用锁,就不能使用带资源的 try 语句。首先,解锁方法名不是 close 。不过,即便将它重命名,带资源的 try 语句也没法正常工做。它的首部但愿声明一个新变量。可是若是使用一个锁,可能想使用多个线程共享的那个变量(而不是新变量)。
  5. 锁是可重入的,由于线程能够重复地得到已经持有的锁。锁保持一个持有计数( hold count )来跟踪对 lock 方法的嵌套调用。线程在每一次调用lock都要调用 unlock 来释放锁。因为这一特性,被一个锁保护的代码能够调用另外一个使用相同的锁的方法。
  6. 一般,可能想要保护需若干个操做来更新或检查共享对象的代码块。要确保这些操做完成后,另外一个线程才能使用相同对象。
  7. 要留心临界区中的代码,不要由于异常的抛出而跳出了临界区。若是在临界区代码结束以前抛出了异常, finally 子句将释放锁,但会使对象可能处于一种受损状态。
  8. java.util.concurrent.locks.Lock 5.0
    • void lock()
      获取这个锁;若是锁同时被另外一个线程拥有则发生阻塞。
    • void unlock()
      释放这个锁。
  9. java.util.concurrent.locks.ReentrantLock 5.0
    • ReentrantLock()
      构建一个能够被用来保护临界区的可重入锁。
    • ReentrantLock(boolean fair)
      构建一个带有公平策略的锁。一个公平锁偏心等待时间最长的线程。可是么这一公平的保证将大大下降性能。因此,默认状况下,锁没有被强制为公平的。
  10. 听起来公平锁更合理一些,可是使用公平锁比使用常规锁要慢不少。只有当你确实了解本身要作什么而且对于你要解决的问题有一个特定的理由必须使用公平锁的时候,才可使用公平锁。即便使用公平锁,也没法确保线程调度器是公平的。若是线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间,那么就没有机会公平地处理这个锁了。

14.5.4 条件对象

  1. 一般,线程进入临界区,却发如今某一条件知足以后它才能执行。要使用一个条件对象来管理那些已经得到了一个锁可是却不能作有用工做的线程。因为历史的缘由,条件对象常常被称为条件变量( conditional variable )。
  2. 一个锁对象能够有一个或多个相关的条件对象。能够用 newCondition 方法得到一个条件对象。习惯上给每个条件对象命名为能够反映它锁表达的条件的名字。若是条件不知足,调用 Condition.await() 。当前线程如今被阻塞了,并放弃了锁。
  3. 等待得到锁的线程和调用 await 方法的线程存在本质上的不一样。一旦一个线程调用 await 方法,它进入该条件的等待集。当锁可用时,该线程不能立刻解除阻塞。相反,它处于阻塞状态,直到另外一个线程调用同一条件上的 singalAll 方法时为止。 singalAll() 调用从新激活由于这一条件而等待的全部线程。当这些线程从等待集中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图从新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,得到该锁并从被阻塞的地方继承执行。
    此时,线程应该再次测试该条件。因为没法确保该条件被知足 -signalAll 方法仅仅是通知正在等待的线程:此时有可能已经知足条件,值得再次去检测该条件。
  4. 一般,对 await 的调用应该在以下形式的循环体中:
    while(!(ok to proceed))
         condition.await();
  5. 相当重要的是最终须要某个其余线程调用 signalAll 方法。当一个线程调用 await 时,它没有办法从新激活自身。它寄但愿于其余线程。若是没有其余线程来从新激活等待的线程,它就永远再也不运行了。这将致使使人不快的死锁( deadlock )现象。若是全部其余线程被阻塞,最后一个活动线程在解除其余线程的阻塞状态以前就调用 await 方法,那么它也被阻塞。没有任何线程能够解除其余线程的阻塞,那么该程序就挂起了。
  6. 应该什么时候调用 signalAll 呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用 signalAll 。
  7. 注意调用 signalAll 不会当即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程能够在当前线程退出同步方法以后,经过竞争实现对对象的访问。
  8. 另外一个方法 signal ,则是随机解除等待集中某个线程的阻塞状态。这比解除全部线程的阻塞更加有效,但也存在危险。若是随机选择的线程发现本身仍然不能运行,那么它再次被阻塞。若是没有其余线程再次调用 signal ,那么系统就死锁了。
  9. 当一个线程拥有某个条件的锁时,它仅仅能够在该条件上调用 await 、 signalAll 或 signal 方法。
  10. java.util.concurrent.locks.Lock 5.0
    • Condition newCondition()
      返回一个与该锁相关的条件对象。
  11. java.util.concurrent.locks.Condition 5.0
    • void await()
      将该线程放到条件的等待集中。
    • void signalAll()
      解除该条件的等待集中的全部线程的阻塞状态。
    • void signal()
      从该条件的等待集中随机地选择一个线程,解除其阻塞状态。

14.5.5 synchronized关键字

  1. 锁和条件的关键之处:
    • 锁用来保护代码片断,任什么时候刻只能有一个线程执行被保护的代码。
    • 锁能够管理试图进入被保护代码段的线程。
    • 锁能够拥有一个或多个相关的条件对象。
    • 每一个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
  2. 从 1.0 版开始, Java 中的每个对象都有一个内部锁。若是一个方法用 synchronized 关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须得到内部的对象锁。
  3. 内部对象锁只有一个相关条件。 wait 方法添加一个线程到等待集中, notifyAll/notify 方法解除等待线程的阻塞状态。换句话说,调用 wait 或 notifyAll 等价于
    intrinsicCondition.await();
     intrinsicCondition.signalAll();
  4. wait 、 notifyAll 以及 notify 方法是 Object 类的 final 方法。 Condition 方法必须被命名为 await 、 signalAll 和 signal 以便它们不会与那些方法发生冲突。
  5. 将静态方法声明为 synchronized 也是合法的。若是调用这个方法,该方法得到相关的类对象的内部类。所以,没有其余线程能够调用同一个类的这个或任何其余的同步静态方法。
  6. 内部锁和条件存在一些局限。包括:
    • 不能中断一个正在试图得到锁的线程。
    • 试图得到锁时不能设定超时。
    • 每一个锁仅有单一的条件,多是不够的。
  7. 在代码中应该使用哪种? Lock 和 Conditon 对象仍是同步方法?下面是一些建议:
    • 最好既不使用 Lock/Condition 也不实用 synchronized 关键字。在许多状况下你可使用 java.util.concurrent 包中的一种机制,它会为你处理全部的加锁。
    • 若是 synchronized 关键字适合你的程序,那么请尽可能使用它。这样能够减小编写的代码数量,减小出错的概率。
    • 若是特别须要 Lock/Condition 结构提供的特有特性时,才使用 Lock/Condition 。
  8. 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

14.5.6 同步阻塞

  1. 每个 Java 对象有一个锁。线程能够经过调用同步方法得到锁。还有另外一种机制能够得到锁,经过进入一个同步阻塞。当线程进入以下形式的阻塞:
    synchronized(obj) //this is the     synchronized block
     {
         critical section
     }
    因而它得到 obj 的锁。
  2. 使用一个对象的锁来实现额外的原子操做,实际上称为客户端锁定( client-slide locking )。客户端锁定是很是脆弱的,一般不推荐使用。

14.5.7 监视器概念

  1. 锁和条件是线程同步的强大工具,可是,严格来说,它们不是面向对象的。多年来,研究人员努力寻找一种方法,能够在不须要程序员考虑如何加锁的状况下,就能够保证多线程的安全性。最成功的解决方案之一是监视器( monitor ),这一律念最先是由 Per Brinch Hansen 和 Tony Hoare 在 20 世纪 70 年代提出的。用 Java 的术语来说,监视器具备以下特性:
    • 监视器是只包含私有域的类。
    • 每一个监视器的对象有一个相关的锁。
    • 使用该锁对全部的方法进行加锁。
    • 该锁能够有任意多个相关条件。
  2. Java 设计者以不是很精确的方式采用了监视器概念, Java 中的每个对象有一个内部的锁和内部的条件。若是一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监视器方法。经过调用 wait/notifyAll/notify 访问条件变量。
  3. 在下述的 3 个方面 Java 对象不一样于监视器,从而使得线程的安全性降低。
    • 域不要求必须是 private 。
    • 方法不要求必须是 synchronized 。
    • 内部锁对客户是可用的。

14.5.8 Volatile 域

  1. 有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。毕竟,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大。
    • 多处理器的计算机可以暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不一样处理器上的线程可能在同一个内存位置取到不一样的值。
    • 编译器能够改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,可是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值能够被另外一个线程改变。
      若是你使用锁来保护能够被多个线程访问的代码,那么能够不考虑这种问题。编译器被要求经过在必要的时候刷新本地缓存来保持锁的效应,而且不能不正当地从新排序指令。
  2. volatile 关键字为实例域的同步访问提供了一种免锁机制。若是声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另外一个线程并发更新的。
  3. Volatile 变量不能提供原子性。例如,方法
    private volatile boolean done;
    public void flipDone(){done = !done;} //not atomic
    不能确保翻转域中的值。

14.5.9 final 变量

  1. 除非使用域或 volatile 修饰符,不然没法从多个线程安全地读取一个域。还有一种状况能够安全地访问一个共享域,即这个域声明为 final 时。考虑如下声明:
    final Map<String,Double> accounts = new HashMap();
    其余线程会在构造函数完成构造以后才看到这个 accounts 变量。
    若是不使用 final ,就不能保证其余线程看到的是 accounts 更新后的值,它们可能都只是看到 null ,而不是新构造的 HashMap 。
    对这个映射表的操做并非线程安全的。若是多个线程在读写这个映射表,仍然须要进行同步。

14.5.10 原子性

  1. 假设对共享变量除了赋值以外并不完成其余操做,那么能够将这些共享变量声明为 volatic 。
  2. java.util.concurrent.atomic 包中有不少类使用了很高效的机器级指令(而不是使用锁)来保证其余操做的原子性。例如, AtomicInteger 类提供了方法 incrementAndGet 和 decrementAndGet ,它们分别以原子方式将一个整数自增或自减。能够安全地使用 AtomicInteger 做为共享计数器而无须同步。
    另外这个包中还包含 AtomicBoolean 、 AtomicLong 和 AtomicReference 以及 Boolean 值、整数、 long 值和引用的原子数组。应用程序员不该该使用这些类,它们仅供那些开发并发工具的系统程序员使用。

14.5.11 死锁

  1. 有可能会由于每个线程要等待条件而致使全部线程都被阻塞。这样的状态称为死锁( deadlock )。
  2. Java 编程语言中没有任何东西能够避免或打破这种死锁现象,必须仔细设计程序,以确保不会出现死锁。

14.5.12 线程局部变量

  1. 有时可能要避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。
  2. 要为每一个线程构造一个实例,可使用如下代码:
    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 类实例。
  3. java.lang.ThreadLocal< T > 1.2
    • T get()
      获得这个线程的当前值。若是是首次调用get,会调用 initialize 来获得这个值。
    • protected initialize()
      应覆盖这个方法来提供一个初始值。默认状况下,这个方法返回 null 。
    • void set(T t)
      为这个线程设置一个新值。
    • void remove()
      删除对应这个线程的值。
  4. java.util.concurrent.ThreadLocalRandom 7
    • static ThreadLocalRandom current()
      返回特定于当前线程的 Random 类实例。

14.5.13 锁测试与超时

  1. 线程在调用 lock 方法来得到另外一个线程所持有的锁的时候,极可能发生阻塞。应该更加谨慎地申请锁。
  2. TimeUnit 是一个枚举类型,能够取的值包括 SECONDS 、 MILLISECONDS 、 MICROSECONDS 和 NANOSECONDS 。
  3. lock 方法不能被中断。若是一个线程在等待得到一个锁时被中断,中断线程在得到锁以前一直处于阻塞状态。若是出现死锁,那么, lock 方法就没法终止。
  4. 然而,若是调用带有用超时参数的 tryLock ,那么若是线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个很是有用的特性,由于容许程序打破死锁。
  5. 也能够调用 lockInterruptibly 方法。它就至关于一个超时设为无限的 tryLock 方法。
  6. 在等待一个条件时,也能够提供一个超时:
    myCondition.await(100,TimeUnit.MILLISECONDS)
    若是一个线程被另外一个线程经过调用 signalAll 或 signal 激活,或者超时时限已达到,或者线程被中断,那么 await 方法将返回。
    若是等待的线程被中断, await 方法将抛出一个 InterruptedException 异常。在你但愿出现这种状况时线程继续等待(可能不太合理),可使用 awaitUniterruptibly 方法代替 await 。
  7. java.util.concurrent.locks.Lock 5.0
    • boolean tryLock()
      尝试得到锁而没有发生阻塞;若是成功返回真。这个方法会抢夺可用的锁,即便该锁有公平加锁策略,即使其余线程已经等待好久也是如此。
    • boolean tryLock(long time,TimeUnit unit)
      尝试得到锁,阻塞时间不会超过给定的值;若是成功返回 true 。
    • void lockInterruptibly()
      得到锁,可是会不肯定地发生阻塞。若是线程被中断,抛出一个 InterruptedException 异常。
  8. java.util.concurrent.locks.Condition 5.0
    • boolean await(long time,TimeUnit unit)
      进入该条件的等待集,直到线程从等待集中移出或等待了指定的时间以后才解除阻塞。。若是由于等待时间到了而返回就返回 false ,不然返回 true 。
    • void awaitUninterruptinly()
      进入该条件的等待集,直到线程从等待集移出才解除阻塞。若是线程被中断,该方法不会抛出 InterruptedException 异常。

14.5.14 读/写锁

  1. java.util.concurrent.locks包定义了两个锁类, ReentrantLock 类和 ReentrantReadWriteLock 类。若是不少线程从一个数据结构读取数据而不多线程修改其中数据的话,后者是十分有用的。在这种状况下,容许对读者共享访问是合适的。固然,写者线程依然必须是互斥访问的。
  2. 使用读/写锁的必要步骤:
    (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();}
     }
  3. java.util.concurrent.locks.ReentrantReadWriteLock 5.0
    • Lock readLock()
      获得一个能够被多个读操做共用的读锁,但会排斥全部写操做。
    • Lock writeLock()
      获得一个写锁,排斥全部其余的读操做和写操做。

14.5.15 为何弃用stop和suspend方法

  1. 初始的Java版本定义了一个 stop 方法用来终止一个线程,以及一个 suspend 方法来阻塞一个线程直至另外一个线程调用 resume 。 stop 和 suspend 方法有一些共同点:都试图控制一个给定线程的行为。
    这两个方法已经弃用。 stop 方法天生就不安全,经验证实 suspend 方法会常常致使死锁。
  2. stop 方法终止全部未结束的方法,包括 run 方法。当线程被终止,当即释放被它锁住的全部对象的锁。这会致使对象处于不一致的状态。
    当线程要终止另外一个线程时,没法知道何时调用 stop 方法是安全的,何时致使对象被破坏。所以,该方法被弃用了。在但愿中止线程的时候应该中断线程,被中断的线程会在安全的时候中止。
    一些做者声称 stop 方法被弃用是由于它会致使对象被一个已中止的线程永久锁定。可是,这一说法是错误的。从技术上讲,被中止的线程经过抛出 ThreadDeath 异常退出全部它所调用的同步方法。结果是,该线程释放它持有的内部对象锁。
  3. 与 stop 不一样, suspend 不会破坏对象。可是,若是用 suspend 方法的线程试图得到同一个锁,那么,该锁在恢复以前是不可用的。若是调用 suspend 方法的线程试图得到同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待得到锁。
  4. 若是想安全地挂起线程,引入一个变量 suspendRequested 并在 run 方法的某个安全的地方测试它,安全的地方是指该线程没有封锁其余线程须要的对象的地方。当该线程发现 suspendRequested 变量已经设置,将会保持等待状态直到它再次得到为止。

14.6 阻塞队列

  1. 对于许多线程问题,能够经过使用一个或多个队列以优雅且安全的方式将其形式化。
  2. 当试图向队列添加元素而队列已满,或是想从队列移除元素而队列为空的时候,阻塞队列( bolcking queue )致使线程阻塞。在协调多个线程以前的合做时,阻塞队列是一个有用的工具。工做者线程能够周期性地将中间结果存储在阻塞队列中。其余的工做者线程移出中间结果并进一步加以修改。队列会自动的平衡负载。若是第一个线程集运行得比第二个慢,第二个线程集在等待结果时会阻塞。若是第一个线程集运行得快,它将等待第二个队列集遇上来。
  3. 阻塞队列方法
    方法 正常动做 特殊状况下的动做
    add 添加一个元素 若是队列满,则抛出IllegalStateException异常
    element 返回队列的头元素 若是队列空,抛出NoSuchElementException异常
    offer 添加一个元素并返回true 若是队列满,返回false
    peek 返回队列的头元素 若是队列空,则返回null
    poll 移出并返回队列的头元素 若是队列空,则返回null
    put 添加一个元素 若是队列满,则阻塞
    remove 移出并返回头元素 若是队列空,则抛出NoSuchElementException异常
    take 移出并返回头元素 若是队列空,则阻塞
  4. 阻塞队列方法分为如下 3 类,这取决于当队列满或空时它们的响应方式。若是将队列看成线程管理工具来使用,将要用到 put 和 take 方法。当试图向满的队列中添加或从空的队列中移出元素时, add 、remove和element操做抛出异常。固然,在一个多线程程序中,队列会在任什么时候候空或满,所以,必定要使用offer、poll和peek方法做为替代。这些方法若是不能完成任务,只是给出一个错误提示而不会抛出异常。
  5. poll和peek方法返回空来指示失败。所以,向这些队列中插入null值是非法的。
  6. 还有带有超市的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方法等效。
  7. java.util.concurrent包提供了阻塞队列的几个变种。默认状况下,LinkedBlockingQueue的容量是没有上边界的,可是,也能够选择指定最大容量。LinkedBlockingDeque是一个双端的版本。ArrayBlockingQueue在构造时须要指定容量,而且有一个可选的参数来指定是否须要公平性。若设置了公平参数,则那么等待了最长时间的线程会优先获得处理。一般,公平性会下降性能,只有在确实很是须要时才使用它。
  8. PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,可是,若是队列是空的,取元素的操做会阻塞。
  9. DelayQueue包含是Delayed接口的对象:
    interface Delayed extends Comparable< Delayed >
     {
         long getDelay(TimeUnit unit);
     }
    getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只有在延迟用完的状况下才能从DelayQueue移除。还必须实现compareTo方法。DelayQueue使用该方法对元素进行排序。
  10. Java SE 7增长了一个TransferQueue接口,容许生产者线程等待,直到消费者准备就绪能够接收一个元素。若是生产者调用
    q.transfer(item);
    这个调用会阻塞,直到另外一个线程将元素(item)删除。LinkedTransferQueue实现了这个接口。
  11. java.util.concurrent.ArrayBlockingQueue< E > 5.0
    • ArrayBlockingQueue(int capacity)
    • ArrayBlockingQueue(int capacity,boolean fair)
      构造一个带有指定的容量和公平性的阻塞队列。该队列用循环数组实现。
  12. java.util.concurrent.LinkedBlockingQueue< E > 5.0
    java.uti..concurrent.LinkedBlockingDeque< E > 6.0
    • LinkedBlockingQueue()
    • LinkedBlockingDeque()
      构造一个无上限的阻塞队列或双向队列,用链表实现。
    • LinkedBolckingQueue(int capacity)
    • LinkedBlockingDeque(int capacity)
      根据指定容量构建一个有限的阻塞队列或双向队列,用链表实现。
  13. java.util.concurrent.DelayQueue< E extends Delayed > 5.0
    • DelayQueue()
      构造一个包含Delayed元素的无界的阻塞时间有限的阻塞队列。只有那些延迟已经超过期间的元素能够从队列中移出。
  14. java.util.concurrent.Delayed 5.0
    • long getDelay(TimeUnit unit)
      获得该对象的延迟,用给定的时间单位进行度量。
  15. java.util.concurrent.PriorityBlockingQueue< E > 5.0
    • PriorityBlockingQueue()
    • PriorityBlockingQueue(int initialCapacity)
    • PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator)
      构造一个无边界阻塞优先队列,用堆实现。
      参数:initialCapacity 优先队列的初始容量。默认值是11。
      comparator 用来对元素进行比较的比较器,若是没有指定,则元素必须实现Comparable接口。
  16. 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。
  17. 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。
  18. java.util.concurrent.TransferQueue< E > 7
    • void transfer(E element)
    • boolean tryTransfer(E element,long time,TimeUnit unit)
      传输一个值,或者尝试在给定的超时时间内传输这个值,这个调用将阻塞,直到另外一个线程将元素删除。第二个方法会在调用成功时返回true。

14.7 线程安全的集合

  1. 能够经过提供锁来保护共享数据结构,可是选择线程安全的实现做为替代可能更容易写。

14.7.1 高效的映射表、集合和队列

  1. java.util.concurrent包提供了映射表、有序集合队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。这些集合使用复杂的算法,经过容许并发地访问数据结构的不一样部分来使竞争极小化。
  2. 与大多数集合不一样,size方法没必要在常量时间内操做。肯定这样的集合当前的大小一般须要遍历。
  3. 集合返回弱一致性(weakly consisteut)的迭代器。这意味着迭代器不必定能反映出它们被构造以后的全部的修改,可是,它们不会将同一个值返回两次,也不会抛出Concurrent ModificationException异常。
  4. 与之造成对照的是,集合若是在迭代器构造以后发生改变,java.util包中的迭代器将抛出一个ConcurrentModificationException异常。
  5. 并发地散列映射表,可高效地支持大量的读者和必定数量的写者。默认状况下,假定能够有多达16个写者线程同时执行。能够有更多的写者线程,可是,若是同一时间多于16个,其余线程将暂时被阻塞。能够指定更大数目的构造器,然而,没有这种必要。
  6. ConcurrentHashMap和ConcurrentSkipListMap类有相应的方法用于原子性的关联插入以及关联删除。putIfAbsent方法自动地添加新的关联,前提是原来没有这一关联。对于多线程访问的缓存来讲这是颇有用的,确保只有一个线程向缓存添加项:
    cache.putIfAbsent(key,value);
    相反的操做是删除(或许应该叫作removeIfPresent)。调用
    cache.remove(key,value);
    将原子性地删除键值对,若是它们在映像表中出现的话。最后,
    cache.replace(key,oldValue,newValue);
    原子性地用新值替换旧值,假定旧值与指定的键值关联。
  7. java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
    • ConcurrentLinkedQueue< E >()
      构造一个能够被多线程安全访问的无边界非阻塞的队列。
  8. java.util.concurrent.ConcurrentLinkedQueue< E > 5.0
    • ConcurrentSkipListSet< E >()
    • ConcurrentSkipListSet< E >(Comparator<? super E> comp)
      构造一个能够被多线程安全访问的有序集。第一个构造器要求元素实现Comparable接口。
  9. 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 写数组的拷贝

  1. CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中全部的修改线程对底层数组进行复制。若是在集合上进行迭代的线程数超过修改线程数,这样的安排是颇有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用。若是数组后来被修改了,迭代器仍然引用旧数组,可是,集合的数组已经被替换了。所以,旧的迭代器拥有一致的(可能过期的)视图,访问它无须同步开销。

14.7.3 较早的线程安全集合

  1. 从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 >());
  2. 结果集合的方法使用锁加以保护,提供了线程的安全访问。
  3. 应该确保没有任何线程经过原始的非同步方法访问数据结构。最便利的方法是确保不保存任何指向原始对象的引用,简单地构造一个集合并当即传递给包装器。
  4. 若是在另外一个线程可能进行修改时要对集合进行迭代,仍然须要使用“客户端”锁定:
    synchronized(synchHashMap)
     {
         Iterator< K > iter = synchHashMap.keySet().iterator();
         while(iter.hashNext())...;
     }
  5. 若是使用“for each”循环必须使用一样的代码,由于循环使用了迭代器。注意:若是在迭代过程当中,别的线程修改集合,迭代器会失效,抛出ConcurrentModificationException异常。同步仍然是须要的,所以并发的修改能够被可靠地检测出来。
  6. 最好使用java.util.concurrent包中的集合,不使用同步包装器中的。特别是,加入它们访问的是不一样的桶,因为ConcurrentHashMap已经精心地实现了,多线程能够访问它并且不会彼此阻塞。有一个例外是常常被修改的数组列表。在那种状况下,同步的ArrayList能够赛过CopyOnWriteArrayList。
  7. 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

  1. Runnable封装一个异步运行的任务,能够把它想一想成为一个没有参数和返回值的异步方法。Callable与Runnable相似,可是有返回值。Callable接口是一个参数化的类型,只有一个方法call。
    public interface Callable< V >
     {
         V call() throws Exception;
     }
    类型参数是返回值的类型。例如,Callable< Integer >表示一个最终返回Integer对象的异步计算。
  2. Future保存异步计算的结果。能够启动一个计算,将Future对象交给某个线程,而后忘掉它。Future对象的全部者在结果计算好以后就能够得到它。
  3. 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,它就被中断。
  4. FutureTask包装器是一种很是便利的机制,可将Callable转换成Future和Runnable,它同时实现两者的接口。
  5. java.util.concurrent.Callable< V > 5.0
    • V call()
      运行一个将产生结果的任务。
  6. 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。
  7. java.util.concurrent.FutureTask< V > 5.0
    • FutureTask(Callable< V > task)
    • FutureTask(Runnable task,V result)
      构造一个既是Future< V >又是Runnable的对象。

14.9 执行器

  1. 构造一个新的线程是由必定代价的,由于涉及与操做系统的交互。若是程序中建立了大量的生命周期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
  2. 另外一个使用线程池的理由是减小并发线程的数目。建立大量线程会大大下降性能甚至使虚拟机崩溃。若是有一个会建立许多线程的算法,应该使用线程数“固定的”线程池以限制并发线程的总数。
  3. 执行器(Executor)类有许多静态工厂方法用来构建线程池。
  4. 执行者工厂方法
    方法 描述
    newCachedThreadPool 必要时建立新线程;空闲线程会被保留60秒
    newFixedThreadPool 该池包含固定数量的线程;空闲线程会一直被保留
    newSingleThreadExecutor 只有一个线程的“池”,该线程顺序执行每个提交的任务(相似于Swing事件分配线程)
    newScheduledThreadPool 用于预约执行而构建的固定线程池,替代java.util.Timer
    newSingleThreadScheduleExecutor 用于预约执行而构建的单线程“池”

14.9.1 线程池

  1. newCachedThreadPool方法构建了一个线程池。对于每一个任务,若是有空闲线程可用,当即让它执行任务,若是没有可用的空闲线程,则构建一个新线程。newFixedThreadPool方法构建一个具备固定大小的线程池。若是提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中。当其余任务完成之后再运行它们。newSingleThreadExecutor是一个退化了的大小为1的线程池:由一个线程执行提交的任务,一个接着一个。这3个方法返回实现了ExecutorService接口的ThreadPoolExecutor类的对象。
  2. 可用下面的方法之一将一个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对象将在计算结果准备好的时候获得它。
  3. 当用完一个线程池的时候,调用shutdown。该方法启动该池的关闭序列。被关闭的执行器再也不接受新的任务。当全部任务都完成之后,线程池中的线程死亡。另外一个方法是调用shutdownNow。该池取消还没有开始的全部任务并试图中断正在运行的线程。
  4. 下面总结了在使用链接池时应该作的事:
    (1)调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool。
    (2)调用submit提交Runnable或Callable对象。
    (3)若是想要取消一个任务,或若是提交Callable对象,那就要保存好返回的Future对象。
    (4)当再也不提交任何任务时,调用shutdown。
  5. java.util.concurrent.Executors 5.0
    • ExecutorService newCachedThreadPool()
      返回一个带缓存的线程池,该池在必要的时候建立线程,在线程空闲60秒以后终止线程。
    • ExecutorService newFixedThreadPool(int threads)
      返回一个线程池,该池中的线程数由参数指定。
    • ExecutorService newSingleThreadExecutor()
      返回一个执行器,它在一个单个的线程中一次执行各个任务。
  6. 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()
      关闭服务,会先按成已经提交的任务而再也不接收新的任务。
  7. java.util.concurrent.ThreadPoolExecutor 5.0
    • int getLargestPoolSize()
      返回线程池在该执行器盛行周期中的最大尺寸。

14.9.2 预约执行

  1. ScheduleExecutorService接口具备为预约执行(Scheduled Execution)或重复执行任务而设计的方法。它是一种容许使用线程池机制的java.util.Timer的泛化。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象。
  2. 能够预约Runnable或Callable在初始的延迟以后只运行一次。也能够预约一个Runnable对象周期性地运行。
  3. java.util.concurrent.Executors 5.0
    • ScheduledExcutorService newScheduledThreadPool(int threads)
      返回一个线程池,它使用给定的线程数来调度任务。
    • ScheduledExecutorService newSingleThreadScheduledExecutor()
      返回一个执行器,它在一个单独线程中调度任务。
  4. 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 控制任务组

  1. invokeAny方法提交全部对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。没法知道返回的到底是哪一个任务的结果,也许是最早完成的那个任务的结果。对于搜素问题,若是你愿意接受任何一种解决方案的话,就可使用这个方法。
  2. invokeAll方法提交全部对象到一个Callable对象的集合中,并返回一个Future对象的列表,表明全部任务的解决方案。当计算结果可得到时,能够像下面这样对结果进行处理:
    List<Callable< T >> tasks=...;
     List<Future< T >> results =             executor.invokeAll(tasks);
     for (Future< T > result:results)
         processFurther(result.get());
    这个方法的缺点是若是第一个任务恰巧花去了不少时间,则可能按可得到的顺序保存起来更有实际意义。能够用ExecutorCompletionService来进行排列。
  3. 用常规的方法得到一个执行器。而后,构建一个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());
  4. 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超时。
  5. 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。第二个方法将等待给定的时间。

14.9.4 Fork-Join框架

  1. 有些应用使用了大量线程,但其中大多数都是空闲的。举例来讲,一个Web服务器可能会为每一个链接分别使用一个线程。另一些应用可能对每一个处理器内核分别使用一个线程,来完成计算密集型任务,如图像或视频处理。Java SE 7中新引入了fork-join框架,专门用来支持后一类应用。假设有一个处理任务,它能够很天然地分解为子任务,以下所示:
    if(problemSize > threshold)
         solve problem direckly
     else
     {
         break problem into subproblems
         recursively solve each subproblem
         combine the results
     }
  2. 在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工做负载,这种方法称为工做密取(work stealing)。每一个工做线程都有一个双端队列(deque)来完成任务。一个工做线程将子任务压入其双端队列的队头。(只有一个线程能够访问队头,因此不须要加锁。)一个工做线程空闲时,它会从另外一个双端队列的队尾“密取”一个任务。因为大的子任务都在队尾,这种密取不多出现。

14.10 同步器

  1. java.util.concurrent包包含了几个能帮助人们管理相互合做的线程集的类见下表。这些机制具备为线程之间的共用集结点模式(common rendezous patterns)提供的“预置功能”(canned functionality)。若是有一个相互合做的线程集知足这些行为模式之一,那么应该直接重用合适的库类而不要试图提供手工的锁与条件的集合。
    同步器
    它能作什么 什么时候使用
    CyclicBarrier 容许线程集等待直至其中预约数目的线程到达一个公共障栅(barrier),而后能够选择执行一个处理障栅的动做 当大量的线程须要在它们的结果可用以前完成时
    CountDownLatch 容许线程集等待直到计数器减为0 当一个或多个线程须要等待直到指定数目的事件发生
    Exchanger 容许两个线程在要交换的对象准备好时交换对象 当两个线程工做在同一个数据结构的两个实例上的时候,一个向实例添加数据而另外一个从实例清除数据
    Semaphore 容许线程集等待直到被容许继续运行为止 限制访问资源的线程总数。若是许可数是1,经常阻塞线程直到另外一个线程给出许可为止
    SynchronoutQueue 容许一个线程把对象交给另外一个线程 在没有显式同步的状况下,当两个线程准备好将一个对象从一个线程传递到另外一个时

14.10.1 信号量

  1. 概念上讲,一个信号量管理许多的许可证( permits )。为了经过信号量,线程经过调用 acquire 请求许可。其实没有实际的许可对象,信号量仅维护一个计数。许可的数目是固定的,由此限制了经过的线程数量。其余线程能够经过调用 release 释放许可。并且,许可不是必须由获取它的线程释放。事实上,任何线程均可以释听任意数目的许可,这可能会增长许可数目以致于超出初始数目。
  2. 信号量在 1968 年由 Edsger Dijkstra 发明,做为同步原语( synchronization primitive )。 Dijkstra指出信号量能够被有效地实现,而且有足够的能力解决许多常见的线程同步问题。在几乎任何一本操做系统教科书中,都能看到使用信号量实现的有界队列。
    应用程序员没必要本身实现有界队列。一般,信号量没必要直接映射到通用应用场景。

14.10.2 倒计时门栓

  1. 一个倒计时门栓( CountDownLatch )让一个线程集等待直到计数变为 0 。倒计时门栓是一次性的。一旦计数为 0 ,就不能再重用了。
  2. 一个有用的特例是计数值为 1 的门栓。实现一个只能经过一次的门。线程在门外等候直到另外一个线程将计数器值置为 0 。

14.10.3 障栅

  1. CyclicBarrier 类实现了一个集结点( rendezvous )称为障栅( barrier )。考虑大量线程运行在一次计算的不一样部分的情形。当全部部分都准备好时,须要把结果组合在一块儿。当一个线程完成了它的那部分任务后,咱们让它运行到障栅处。一旦全部的线程都到达了这个障栅,障栅就撤销,线程就能够继续运行。
  2. 若是任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可能离开是由于它调用 await 时设置了超时,或者由于它被中断了)。在这种状况下,全部其余线程的 await 方法抛出 BrokenBarrierException 异常。那些已经在等待的线程当即终止 await 的调用。
  3. 障栅被称为是循环的( cyclic ),由于能够在全部等待线程被释放后被重用。在这一点上,有别于 CountDownLatch , CountDownLatch 只能被运行一次。
  4. Phaser 类增长了更大的灵活性,云溪改变不一样阶段中参与线程的个数。

14.10.4 交换器

  1. 当两个线程在同一个数据缓冲区的两个实例上工做的时候,就可使用变换器( Exchanger )。典型的状况是,一个线程想缓冲区填入数据,另外一个线程消耗这些数据。当它们都完成之后,相互交换缓冲区。

14.10.5 同步队列

  1. 同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另外一个线程调用 take 方法为止,反之亦然。与 Exchanger 的状况不一样,数据仅仅沿一个方向传递,从生产者到消费者。
  2. 即便 SynchronousQueue 类实现了 BlockingQueue 接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的 size 方法老是返回 0 。

14.11 线程与Swing

  1. 在程序中使用线程的理由之一是提升程序的响应性能。当程序须要作某些耗时的工做时,应该启动另外一个工做器线程而不是阻塞用户接口。
  2. 必须认真考虑工做器线程在作什么,由于这或许使人惊讶, Swing 不是线程安全的。若是试图在多个县城中操纵用户界面的元素,那么用户界面可能奔溃。

14.11.1 运行耗时的任务

  1. 将线程与 Swing 一块儿使用时,必须遵循两个简单的原则。java

    • 若是一个动做须要花费很长时间,在一个独立的工做器线程中作这件事不要在事件分配线程中作。
    • 除了事件分配线程,不要在任何线程中接触 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 方法。没有新的线程被建立。程序员

  2. 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。

14.11.2 使用Swing工做线程

  1. SwingWorker 类覆盖 doInBackground 方法来完成耗时的工做,调用 publish 来报告工做进度。这一方法在工做器线程中执行。 publish 方法使得 process 方法在事件分配线程中执行来处理进度数据。当工做完成时, done 方法在事件分配线程中被调用以便完成 UI 的更新。
      每当要在工做器线程中作一些工做时,构建一个新的工做器(每个工做器对象只能被使用一次)。而后调用 execute 方法。典型的方式是在事件分配线程中调用 execute ,但没有这样的需求。
  2. SwingWorker 类有 3 种类型做为类型参数。 SwingWorker< T,V > 产生类型为 T 的结果以及类型为 V 的进度数据。
  3. 要取消正在进行的工做,使用 Future 接口的 cancel 方法。当该工做被取消的时候, get 方法抛出 CancellationException 异常。
  4. 工做器线程对 publish 的调用会致使在事件分配线程上的 process 的调用。为了提升效率,几个对 publish 的调用结果,可用对 process 的一次调用成批处理。 process 方法接收一个包含全部中间结果的列表< V > 。
  5. 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 之一。

14.11.3 单一线程规则

  1. 单一线程规则:“除了事件分配线程,不要在任何线程中接触 Swing 组件”。
  2. 对于单一线程规则存在一些例外状况:
    • 可在任一个线程里添加或移除事件监听器。固然该监听器的方法会在事件分配线程中被触发。
    • 只有不多的 Swing 方法是线程安全的。在 API 文档中用这样的句子特别标明: “尽管大多数 Swing 方法不是线程安全的,但这个方法是。”在这些线程安全的方法中最有用的是:
      JTextComponent.setText
      JtextArea.insert
      JTextArea.append
      JTextArea.replaceRange
      JComponent.repaint
      JComponent.revalidate
  3. 历史上,单一线程规则是更加随意的。任何线程均可以构建组件,设置优先级,将它们添加到容器中,只要这些组件没有一个是已经被实现的( realized )。若是组件能够接受 paint 事件或 validation 事件,组件被实现。一旦调用组件的 setVisible(true) 或 pack(!) 方法或者组件已经被添加到已经被实现的容器中,就出现这样的状况。
相关文章
相关标签/搜索