书籍作者:黄文海
出版社:电子工业出版社

思维导图

全文思维导图

第一章 走进Java世界中的线程

  1. 进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。
  2. 一个进程可以包含多个线程。
  3. 线程所要完成的计算被称为任务
  4. 启动一个线程的实质是请求Java虚拟机运行相应的线程,而这个线程具体何时能够运行是由线程调度器决定的。
  5. Java虚拟机会为每个线程分配调用栈所需的内存空间。
  6. Java平台中的任意一段代码总是由确定的线程负责执行的,这个线程就相应的被称为这段代码的执行线程。可以通过调用Thread.currentThread()来获取这段代码的执行线程。
  7. 线程的属性:
属性 类型及用途 注意事项
编号(ID) Long。用于标识不同的线程。 不同线程虽然拥有的编号不同,但是某个编号的线程运行结束后,该编号可能被后续创建的线程使用,这种编号的唯一性只在Java虚拟机的一次运行中有效。
名称(name) String。用于区分不同线程。(面向人)默认值与线程编号有关,默认值格式为:“Thread-线程编号” 为每一个线程设置一个简短而含义明确的名称有助于多线程程序的调试和问题定位。
线程类别(Daemon) boolean。true表示相应的线程为守护线程,否则表示相应的线程为用户线程。 该属性必须在线程启动之前设置,否则setDaemon方法会抛出异常。负责一些关键任务的线程不适合设置为守护线程。
优先级(Priority) int。该属性本质上是给线程调度器的提示,用于表示应用程序希望线程能够优先得以运行。Java定义了1~10的10个优先级,默认值为5。 一般使用默认优先级即可。
  1. 用户线程会阻止Java虚拟机的正常停止,即一个Java虚拟机只有在其所有用户线程都运行结束的情况下才能正常停止。而守护线程则不会影响Java虚拟机的正常停止,守护线程通常用于执行一些重要性不是很高的任务,例如用于监视其他线程的运行情况。
  2. Thread的join方法的作用相当于执行该方法的线程和线程调度器说:“我得先暂停一下,等到另外一个线程运行结束后我才能继续干活。”
  3. yield静态方法的作用相当于执行该方法的线程对线程调度器说:“我现在不急,如果别人需要处理器资源极度话先给他用吧。当然,如果没有其他人要用,我也不介意继续占用。”
  4. sleep静态方法的作用相当于执行该方法的线程对线程调度器说:“我想小憩一会儿,过段时间再叫醒我继续干活吧。”
  5. 假设线程A所执行的代码创建了线程B,那么,习惯上我们称线程B为线程A的子线程,相应地线程A就被称为线程B的父线程
  6. 在Java平台中,一个线程是否是守护线程默认取决于其父线程。
  7. 一个线程的优先级默认值为该线程的父线程的优先级。
  8. Thread.State是一个枚举类型,用来表示线程的当前状态。
    1. NEW:一个已创建而未启动的线程处于该状态。
    2. RUNNABLE:该状态可以被看成一个复合状态。它包括两个子状态:READY和RUNNING。
    3. BLOCKED:线程进行阻塞式操作或申请由其他线程正在独占的资源时,相应的线程会处于该状态。
    4. WAITING:执行某些特定方法之后就会处于这种等待状态,包括:Object.wait(),Thread.join(),LockSupport.park(),Condition.await()。能够使相应线程从WAITING变更为RUNNABLE的相应方法包括:Objeck.notify()/notifyAll()、LockSupport.unpark()、Condition.signal()。
    5. TIMED WAITING:限时等待状态。
    6. TERMINATED:已执行结束的线程处于该状态。
  9. Java程序的线程转储包含的线程具体信息包括线程的属性、生命周期状态、线程的调用栈以及锁相关的信息。
  10. 多线程编程具有以下优势:
    1. 提高系统的吞吐率
    2. 提高响应性
    3. 充分利用多核优势。
    4. 最小化对系统资源的使用。
    5. 简化程序的结构
  11. 多线程编程的风险:
    1. 线程安全问题。
    2. 线程活性问题。
    3. 上下文切换。
    4. 可靠性。

第二章 多线程编程的目标与挑战

  1. 多线程编程的实质就是将任务的处理方式由串行改为并发,即实现并发化,以发挥并发的优势。
  2. 状态变量:即类的实例变量、静态变量。
  3. 共享变量:即可以被多个线程共同访问的变量。
  4. 竞态是指计算的正确性依赖于相对时间顺序或者线程的交错。
  5. 竞态往往伴随着读取脏数据问题。
  6. 竞态的两种模式:read-modify-write(读-改-写)和check-then-act(检测后行动)。
  7. 如果一个类在单线程环境下能够运行正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运作正常,那么我们就称其是线程安全的,相应地,我们称这个类具有线程安全性。
  8. 对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性
  9. Java中有两种方式来实现原子性。一种是使用,另一种是利用处理器提供的专门CAS指令。
  10. 在多线程环境下,一个线程对某个变量进行更新之后,后续访问该变量的线程可能无法立刻读取到这个更新的结果,甚至永远也无法读取到这个更新的结果,这就是线程安全的另一个表现形式:可见性
  11. 可见性问题可能来源于JIT编译器的优化,也可能来源于寄存器高速缓存
  12. 虽然一个处理器的高速缓存中的内容不能被另一个处理器直接读取,但是一个处理器可以通过缓存一致性协议来读取其他处理器的高速缓存的数据,并将读到的数据更新到该处理器的高速缓存中。
  13. volatile关键字所起到的一个作用就是,提示JIT编译器被修饰的变量可能被多个线程共享,以阻止JIT编译器做出可能导致程序运行不正常的优化。另外一个作用是读取一个volatile关键字修饰的变量会使相应的处理器执行刷新处理器缓存的动作,写一个volatile关键字修饰的变量会使相应的处理器执行冲刷处理器缓存的动作,从而保障可见性。
  14. 对于同一个共享变量而言,一个线程更新了该变量的值之后,其他线程能够读取到这个更新后的值,那么这个值就被称为该变量的相对新值。
  15. 父线程在启动子线程之前对共享变量的更新对于子线程来说是可见的。
  16. 一个线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程而言是可见的。
  17. 有序性指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器上运行的其他线程看起来是乱序的。
  18. 重排序是对内存访问有关的操作所做的一种优化,它可以在不影响单线程程序正确性的情况下提升程序的性能。重排序主要包括:指令重排序存储子系统重排序
  19. 重排序可能导致线程安全问题。
  20. 重排序不是必然出现的。
  21. 处理器也可能执行指令重排序,这使得执行顺序和程序顺序不一致,处理器对指令进行重排序也被称为处理器的乱序执行。处理器乱序执行并不会对单线程程序的正确性产生影响。
  22. 主内存相对于处理器是一个慢速设备。为了避免其拖后腿,处理器并不是直接访问主内存,而是通过高速缓存访问主内存的。
  23. 内存重排序包括:LoadLoad重排序、StoreStore重排序、LoadStore重排序、StoreLoad重排序。
  24. 存在数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才可能会被重排序。
  25. 可见性是有序性的基础,有序性影响可见性。
  26. 一个线程由于其时间片用完或者其自身原因被迫或者主动暂停其运行时,另外一个线程可以被操作系统选中占用处理器开始或者继续其运行。这种一个线程暂停,另一个线程被选中开始或者继续运行的过程就叫做上下文切换
  27. 进度信息就被称为上下文,它一般包括通用寄存器的内容和程序计数器的内容。
  28. 自发性上下文切换指线程由于其自身因素导致的切出。如执行以下指令:Thread.sleep()、Object.wait()、Thread.join()、LockSupport.park()。
  29. 非自发性上下文切换指线程由于线程调度器的原因被迫切出。
  30. 上下文切换的开销包括直接开销间接开销
    1. 操作系统保存和恢复上下文所需的开销,这主要是处理器时间开销。
    2. 线程调度器进行线程调度的开销。
    3. 处理器高速缓存重新加载的开销。
    4. 上下文切换也可能导致整个一级高速缓存中的内容被冲刷。
  31. 这些由于资源稀缺性或者程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态,或者线程虽然处于RUNNABLE状态但是其要执行的任务却一直无法进展的现象就被称为线程活性故障。常见的活性故障包括:
    1. 死锁
    2. 锁死
    3. 活锁
    4. 饥饿
  32. 一次只能够被一个线程占用的资源被称为排他性资源。在一个线程占用一个排他性资源进行访问时,其他线程视图访问该资源的现象就被称为资源争用
  33. 同一时间内,处于运行状态的线程数量越多,我们就称并发程度越高,简称高并发
  34. 在多个线程申请同一个排他性资源的情况下,决定哪个线程会被授予该资源的独占权,即选择哪个申请者占用该资源的过程就是资源的调度
  35. 如果资源的任何一个先申请者总是能够比任何一个后申请者先获得该资源的独占权,那么相应的资源调度策略就被称为是公平的,如果资源的后申请者可能比先申请者先获得资源的独占权,那么相应的资源调度策略就被称为非公平的。
  36. 在极端的情况下,非公平调度策略可能导致等待队列中的线程永远无法获得其所需的资源,即出现饥饿
  37. 一般来说,非公平调度策略的吞吐率高,即单位时间内它可以为更多的申请者调配资源。其缺点是,从申请者个体的角度来看这些申请者获得相应资源的独占权所需要的时间偏差可能比较大。
  38. 非公平调度策略可能带来一个好处——减少上下文切换的次数。
  39. 多数线程占用资源的时间相当长的情况下不适合使用非公平调度策略
  40. 非公平调度策略是我们多数情况下的首选调度策略。其优点是吞吐率较大;缺点是资源申请者申请资源所需的时间偏差可能较大,并可能导致饥饿。公平调度策略适合在资源的时间相对长或资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用。其优点是线程申请资源所需的时间偏差较小,并且不会导致线程饥饿现象;其缺点是吞吐率较小。

第三章 Java线程同步机制

  1. 线程同步机制是一套用于协调线程间的数据访问及活动的机制,该机制用用户保障线程安全以及实现这些线程的共同目标。

  2. 线程安全问题的产生前提是多个线程并发访问共享变量、共享资源。

  3. 锁的持有线程在其获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区

  4. Java平台中的锁包括内部锁显示锁

  5. 锁是通过互斥保障原子性的。

  6. 一个锁实例锁保护的共享数据的数量大小就被称为锁的粒度

  7. 锁的开销包括锁的申请和释放锁产生的开销,以及锁可能导致的上下文切换的开销,这些开销主要是处理器时间。

  8. 锁泄漏是指一个线程获得某个锁之后,由于程序的错误缺陷致使该锁一直无法被释放而导致其他线程一直无法获得该锁的现象。

  9. Java平台中的任何一个对象都有唯一的一个与之关联的锁。这种锁被称为监视器或者内部锁。内部锁是一种排他锁,它能保障原子性、可见性和有序性。

  10. Java虚拟机会为每个内部锁分配一个入口集,用于记录等待获得相应内部锁的线程。多个线程申请同一个锁的时候,只有一个申请者能够成为该锁的持有线程,而其他申请者的申请操作会失败。

  11. 公平锁保障锁调度的公平性往往是以增加了线程的暂停和唤醒的可能性,即增加了上下文切换为代价的。因此公平锁适合于锁被持有的时间相对长或者线程申请锁的平均间隔时间相对长的情形。总得来说使用公平锁的开销比使用非公平锁的开销要大,因此显式锁默认使用的是非公平调度策略。

  12. 读写锁是一种改进型的排它锁,也被称为共享/排它锁。读锁是共享的,写锁是排他的。

  13. 读写锁适合于在以下条件同时得以满足的场景中使用:

    1. 只读操作比写操作要频繁得多。
    2. 读线程持有锁的时间比较长。
  14. ReetrantReadWriteLock所实现的读写锁是个可重入锁。ReetrantReadWriteLock支持写锁的降级,即一个线程持有读写锁的写锁的情况下可以继续获得相应的读锁。

  15. ReetrantReadWriteLock并不支持锁的升级。读线程如果要转而申请写锁,需要先释放读锁,然后申请相应的写锁。

  16. 内存屏障是对一类仅针对内存读、写操作指令的跨处理器架构的比较底层的抽象。内存屏障是被插入到两个指令之间进行使用的,其作用是禁止编译器、处理器重排序从而保障有序性

  17. 按照可见性保障来划分,内存屏障可分为加载屏障存储屏障。加载屏障的作用是刷新处理器缓存,存储屏障的作用是冲刷处理器缓存。Java虚拟机会在MonitorExit对应的机器码指令之后插入一个存储屏障,这就保障了写线程在释放锁之前在临界区中对共享变量所做的更新对读线程是可同步的。相应的,Java虚拟机会在MonitorEnter对应的机器码指令之后临界区开始之前的地方插入一个加载屏障,这使得读线程的执行处理器能够将写线程对相应共享变量所做的更新从其他处理器同步到该处理器的高速缓存中。

  18. 按照有序性保障来划分,内存屏障可以分为获取屏障释放屏障。获取屏障的使用方式是在一个读操作之后插入该内存屏障,其作用是禁止该读操作与其后的任何读写操作之间进行重排序,这相当于在进行后续操作之前先要获得相应共享数据的所有权。释放屏障的使用方式是在一个写操作之前插入该内存屏障,其作用是禁止该写操作与其前面的任何读写操作之间进行重排序。Java虚拟机会在MonitorEnter对应的机器码指令之后临界区开始之前的地方插入一个获取屏障,并在临界区结束之后MonitorExit对应的机器码指令之前的地方插入一个释放屏障。

  19. 临界区之外的语句可以被重排序到临界区之内,而临界区内的操作无法被重排序到临界区之外。

    1. 临界区内的操作不允许被重排序到临界区之外。

    2. 临界区内的操作之间允许被重排序。

    3. 临界区外的操作之间可以被重排序。

    4. 锁申请与锁释放操作不能被重排序。

    5. 两个锁申请操作不能被重排序。

    6. 两个锁释放操作不能被重排序。

    7. 临界区外的操作可以被重排到临界区之内。

  20. volatile变量不会被编译器分配到寄存器进行存储,对volatile变量的读写操作都是内存访问操作。

  21. volatile关键字常被称为轻量级锁,其作用与锁的作用有相同的地方:保证可见性和有序性。原子性方面它仅能保证写volatile变量操作的原子性,但没有锁的排他性。其次,volatile关键字的使用不会引起上下文切换。

  22. 一个赋值操作:

    1
    volatile Map aMap = new HashMap();

    可以分解为如下伪代码所示的几个子操作:

    1
    2
    3
    objRef = allocate(HashMap.class); // 子操作1:分配对象所需的存储空间
    invokeConstructor(objRef); // 子操作2:初始化objRef引用的对象
    aMap = objRef; // 子操作3:将对象引用写入变量aMap

    虽然volatile关键字仅保障其中的子操作3是一个原子操作,但是由于子操作1与子操作2仅涉及局部变量而未涉及共享变量,因此对变量aMap的赋值操作仍然是一个原子操作。

  23. 对于volatile变量的写操作,Java虚拟机会在操作之前插入一个释放屏障,并在该操作之后插入一个存储屏障

  24. 对于volatile变量的读操作,Java虚拟机会在操作之前插入一个加载屏障,并在该操作之后插入一个获取屏障

  25. 写volatile变量操作与该操作之前的任何读、写操作不会被重排序

  26. 读volatile变量操作与该操作之后的任何读、写操作不会被重排序

  27. volatile关键字在可见性方面仅仅是保证读线程能够读取到共享变量的相对新值。对于引用型变量和数组变量,volatile关键字并不能保证读线程能够读取到对象相应的字段、元素的相对新值。

  28. volatile变量的读、写操作都不会导致上下文切换,因此volatile的开销比锁要小。

  29. volatile使用的典型场景:

    1. 使用volatile变量作为状态标志

    2. 使用volatile保障可见性

    3. 使用volatile变量代替锁。

    4. 使用volatile实现简易版的读写锁。

  30. volatile关键字并非锁的代替品,volatile关键字和锁各有其适用条件。前者更适合于多个线程共享一个状态变量,而后者更适合于多个线程共享一组状态变量。某些情形下,我们可以将多个线程共享的一组状态变量合并成一个对象,用一个volatile变量来引用该对象,从而使我们不必要使用锁。

  31. 原子变量类是基于CAS实现的能够保障对共享变量进行read-modify-write更新操作的原子性和可见性的一组工具类。

  32. 对象发布是指使对象能够被其作用域之外的线程访问。

  33. 常见的对象发布形式包括:

    1. 将对象引用存储到public变量中。

    2. 在非private方法中返回一个对象。

    3. 创建内部类,使得当前对象能够被这个内部类使用。

    4. 通过方法调用将对象传递给外部方法。

  34. static关键字在多线程环境下有其特殊的含义,它能够保证一个线程即使在未使用其他同步机制的情况下也总是可以读取到一个类的静态变量的初始值。

  35. 对于引用型静态变量,static关键字还能够保障一个线程读取到该变量的初始值时,这个值所指向的对象已经初始化完毕。

  36. static 关键字仅仅保障读线程能够读取到相应字段的初始值,而不是相对新值。

  37. 当一个对象被发布到其他线程的时候,该对象的所有final字段都是初始化完毕的。

  38. 对于引用型final字段,final关键字还进一步确保该字段所引用的对象已经初始化完毕,即这些线程读取该字段所引用的对象的各个字段时所读取到的值都是相应字段的初始值。

  39. 当一个对象的引用对其他线程可见的时候,这些线程所看到的该对象的final字段必然是初始化完毕的。final关键字的作用仅是这种有序性的保障,它并不能保障包含final字段的对象的引用自身对其他线程的可见性。

  40. 安全发布就是指对象以一种线程安全的方式被发布。

  41. 当一个对象的发布出现我们不期望的结果或者对象发布本身不是我们所期望的时候,我们就称该对象逸出

  42. 对象逸出包括:

    1. 在构造函器中将this赋值给一个共享变量。

    2. 在构造器中将this作为方法参数传递给其他方法。

    3. 在构造器中启动基于匿名类的线程。

  43. 一个对象在其初始化过程中没有出现this逸出,我们就称该对象为正确创建的对象。

  44. 实现对象的安全发布,通常可以依照以下顺序选择适用且开销最小的线程同步机制。

    1. 使用staic关键字引用该对象的变量。

    2. 使用final关键字修饰引用该对象的变量。

    3. 使用volatile关键字修饰引用该对象的变量。

    4. 使用AtomicReference来引用该对象。

    5. 对访问该对象的代码进行加锁。

第四章 牛刀小试:玩转线程

  1. 多线程编程中分而治之的使用主要有两种方式:基于数据的分割基于任务的分割

  2. 基于数据的分割的结果是产生多个同质工作者线程,即任务处理逻辑相同的线程。需要考虑如下因素:

    1. 工作者线程数量的合理设置问题。

    2. 工作者线程的异常处理问题。

    3. 原始输入规模未知问题。

    4. 程序的复杂性增加的问题。

  3. 为了提高任务的执行效率,我们可能使用多个线程去共同完成一个任务的执行。这就是基于任务的分割,其基本思想就是将任务按照一定的规则分解成若干子任务,并使用专门的工作者线程去执行这些子任务,从而实现任务的并发执行。

  4. 线程所执行的任务按照 其消耗的主要资源可划分为CPU密集型任务和IO密集型任务。

  5. CPU密集型任务执行过程中消耗的主要资源是CPU时间,CPU密集型任务的一个典型例子是加密和解密;IO密集型任务执行过程中消耗的主要资源是IO资源,典型的IO密集型任务就包括文件读写、网络读写等。

  6. 基于任务的分割结果是产生多个相互协作的异质工作者线程

  7. Amdahl’s 定律描述了线程数与多线程程序相对于单线程程序的提速之间的关系。
    $$
    S_{max} = \frac{1}{P + \frac{1 - P}{N}}
    $$
    其中,N为处理器数量,程序中必须串行化的部分耗时占程序全部耗时的比率为P。

  8. 为使多线程程序能够获得较大的提速,我们应该从算法入手,减少程序中必须串行的部分,而不是寄希望于增加线程数

  9. 线程数设置得过少可能导致无法充分利用处理器资源;而线程数设置得过大则又可能导致过多的上下文切换,从而反倒降低了系统的性能。

  10. 线程数的合理值可以根据以下规则设置:

    1. 对于CPU密集型线程,考虑到这类线程执行任务时消耗的主要是处理器资源,我们可以将这类线程的线程数设置为$N_{cpu} $个。因为CPU密集型线程也可能由于某些原因(比如缺页中断)而被切出,此时为了避免处理器资源的浪费,我们也可以为这类线程设置一个额外的线程,即将线程数设置为$N_{cpu} + 1$

    2. 对于IO密集型线程,考虑到IO操作可能导致上下文切换,为这样的线程设置过多的线程会导致过多的额外系统开销。因此如果一个这样的工作者线程就可以满足我们的要求,那么就不要设置更多的线程数。如果一个工作者线程仍然不够用,那么我们可以考虑将这类线程的数量设置为$2 * N_{cpu} $

  11. 挖掘出程序中可并发点是实现多线程编程的目标——并发计算的前提。

  12. 实现并发化的策略包括基于数据的分割策略和基于任务的分割策略。

第五章 线程间协作

  1. 一个线程因其执行目标动作所需的保护条件未满足而被暂停的过程被称为等待

  2. 一个线程更新了系统的状态,使得其他线程所需的保护条件得以满足的时候唤醒那些被暂停的线程的过程就被称为通知

  3. 由于一个线程只有在持有一个对象的内部所的情况下才能够调用该对象的wait方法,因此Object.wait()调用总是放在相应对象所引导的临界区之中。

  4. 等待线程对保护条件的判断、Object.wait()的执行以及目标动作的执行必须放在同一个对象所引导的临界区之中。

  5. Object.wait()暂停当前线程时释放的锁只是与该wait方法所属对象的内部锁。当前线程所持有的其他内部锁、显示锁并不会因此而被释放。

  6. Object.notify()的执行线程持有的相应对象的内部锁只有在Object.notify()调用所在的临界区代码执行结束后才会被释放,而Object.notify()本身并不会将这个内部锁释放。因此,为了 使等待线程在其被唤醒之后能够尽快再次获得相应的内部锁,我们要尽可能地将Object.notify()调用放在靠近临界区结束的地方。

  7. 等待线程通知线程是同步在同一对象之上的两种线程。

  8. Java虚拟机会为每个对象维护一个入口集用于存储申请该对象内部锁的线程。Java虚拟机还会为每个对象维护一个被称为等待集的队列,该队列用于存储该对象上的等待线程。Object.wait()将当前线程暂停并释放相应的内部锁的同时会将当前线程存入该方法所属对象的等待集中。

  9. wait/notify的开销及问题

    1. 过早唤醒问题

    2. 信号丢失问题

    3. 欺骗性唤醒问题

    4. 上下文切换问题

  10. 只有在有证据表明使用Object.notify()足够的情况下才使用Object.notify(),只有在下列条件全部满足的情况下才能够用于替代notifyAll方法:

    1. 一次通知仅需要唤醒至多一个线程。

    2. 相应对象的等待集中仅包含同质等待线程。

  11. join(long)允许我们指定一个超时时间。如果目标线程没有在指定的时间内终止,那么当前线程也会继续运行。join(long)实际上就是使用了wait/notify来实现的。

  12. Java虚拟机会在目标线程的run方法运行结束后执行该线程的notifyAll方法来通知所有的等待线程。

  13. Condition接口可作为wait/notify的替代品来实现等待/通知,它为解决过早唤醒问题提供了支持,并解决了Object.wait(long)不能区分其返回是否是由等待超时而导致的问题。

  14. Condition.await()/signal()也要求其执行线程持有创建该Condition实例的显示锁。Condition实例也被称为条件变量或者条件队列。每个Condition实例内部都维护了一个用于存储等待线程的队列。

  15. Condition接口本身只是对解决过早唤醒问题提供了支持。要真正解决过早唤醒问题,我们需要通过应用代码维护保护条件与条件变量的await方法来实现其等待,并使通知线程在更新了相关共享变量之后,仅调用与这些共享变量有关的保护条件所对应的条件变量的signal/signalAll方法来实现通知。

  16. Condition.awaitUntil(Date deadline)可以用于实现带超时时间限制的等待,并且该方法的返回值能够区分该方法调用是由于等待超时而返回还是由于其他线程执行了相应条件变量的signal/signalAll方法而返回。

  17. 等待线程因执行Condition.await()/awaitUntil(Date)而被暂停的同时,其持有的相应显示锁也会被释放,等待线程被唤醒之后得以继续运行时需要再次申请相应的显示锁,然后等待线程对Condition.await()/awaitUntil(Date)的调用才能返回。

  18. CountDownLatch可以用来实现一个线程等待其他线程完成一组特定的操作之后才继续运行。这组操作被称为先决操作

  19. CountDownLatch内部计数器值达到0后其值就恒定不变,后续执行该CountDownLatch实例的await方法的任何一个线程都不会被暂停。为了避免等待线程永远被暂停,CountDownLatch.countDown()调用必须放在代码中总是可以被执行到的地方,例如finally块中。

  20. 使用CyclicBarrier实现等待的线程被称为参与方,参与方只需要执行CyclicBarrier.await()就可以实现等待。

  21. 最后一个线程执行CyclicBarrier.await()会使得使用相应CyclicBarrier实例的其他所有参与方被唤醒,而最后一个线程自身并不会被暂停。

  22. 由于CyclicBarrier内部实现是基于条件变量的,因此CyclicBarrier的开销与条件变量的开销相似,其主要开销在可能产生的上下文切换。

  23. CyclicBarrier内部使用了一个条件变量trip来实现等待/通知。CyclicBarrier内部实现使用了分代的概念用于表示CyclicBarrier实例是可以重复使用的。

  24. 最后一个线程相当于通知线程,它执行费CyclicBarrier.await()会使得相应实例的parties值变为0,此时该线程会先执行barrierAction.run(),然后再执行 trip.signalAll()来唤醒所有等待线程。接着,开始下一个分代,即使得CyclicBarrier的parties指又重新恢复为其初始值。

  25. CyclicBarrier的典型应用场景包括以下几个:

    1. 使得迭代算法并发化。

    2. 在测试代码中模拟高并发。

  26. 将产品存入传输通道的线程就被称为生产者线程,从传输通道中取出产品进行消费的线程就被称为消费者线程。

  27. 一个方法或者操作如果能够导致其执行线程被暂停,那么我们就称相应的方法/操作为阻塞方法。阻塞方法能够导致上下文切换。

  28. 阻塞队列按照其存储空间的容量是否受限制来划分,可分为有界队列和无界队列。有界队列的存储容量限制是由应用程序制定的,无界队列的最大存储容量为Interger.MAX_VALUE($2^{31} - 1$)个元素。

  29. ArrayBlockingQueue的缺点是其内部在实现put、take操作的时候使用的是同一个锁,从而可能导致锁的高争用,进而导致较多的上下文切换。

  30. LinkedBlockingQueue既能实现无界队列,也能实现有界队列。

  31. LinkedBlockingQueue的优点是其内部在实现put、take操作的时候分别使用了两个显示锁(putLock和takeLock),这降低了锁争用的可能性。LinkedBlockingQueue的内部存储空间是一个链表,而链表节点所需的存储空间是动态分配的,put操作、take操作都会导致链表节点的动态创建和移除,因此LinkedBlockingQueue的缺点是它可能增加垃圾回收的负担。

  32. SynchronousQueue可以被看做一种特殊的有界队列。

  33. SynchronousQueue适合于在消费者处理能力和生产者处理能力相差不大的情况下使用。

  34. ArrayBlockingQueue和SynchronousQueue都既支持非公平调度也支持公平调度,而LinkedBlockingQueue仅支持非公平调度。
  35. 如果生产者线程和消费者线程之间的并发程度比较大,那么这些线程对传输通道内部所使用的锁的争用可能性也随之增加。这时,有界队列的实现适合选用LinkedBlockingQueue,否则我们可以考虑ArrayBlockingQueue。
  36. 使用无界队列作为传输通道的一个好处是put操作并不会导致生产者线程被阻塞。一般我们在使用无界队列作为传输通道的时候会同时限制生产者的生产速率。
  37. Semaphore.acquire() 和 Semaphore.release()总是配对使用。
  38. Semaphore.release()调用总是应该放在一个finally块中。
  39. 创建Semaphore时如果构造函数中的参数permits值为1,那么所创建的Semaphore实例相当于一个互斥锁。与其他互斥锁不同的是,由于一个线程可以在未执行过Semaphore.acquire()的情况下执行Semaphore.release(),因此这种互斥锁允许一个线程释放另一个线程所持有的锁。
  40. PipedOutputStream和PipedInputStream适合在两个线程间使用,即适用于单生产者-单消费者的情形。
  41. 输出异常的处理。如果生产者线程在其执行过程中出现了不可恢复的异常,那么消费者线程就会永远也无法读取到新的数据。
  42. 当消费者线程消费一个已填充的缓冲区时,另外一个缓冲区可以由生产者线程进行填充,从而实现了数据生成与消费的并发。这种缓冲技术就被称为双缓冲。
  43. Exchanger.exchange(V)的返回值是对方线程执行该方法时所指定的参数x的值。因此,Exchanger.exchange(V)的返回值就造成一种生产者线程和消费者线程之间交换缓冲区的效果。
  44. 中断可以被看做由一个线程发送给另一个线程的一种指示,该指示用于表示发起线程希望目标线程停止其正在执行的操作。中断仅仅代表发起线程的一个诉求,目标线程可能会满足发起线程的诉求,也可能根本不会理会发起线程的诉求。Java平台会为每个线程维护一个被称为中断标记的布尔型状态变量用于表示相应线程释放接收到了中断。
  45. 目标线程检查中断标记后所执行的操作,被称为目标线程对中断的响应,简称中断响应。
  46. 能够响应中断的方法通常是在执行阻塞操作之前判断中断标志,若中断标志值为true则抛出InterruptedException。
  47. 如果发起线程给目标线程发送中断的那一刻,目标线程已经由于执行了一些阻塞方法操作而被暂停,那么此时Java虚拟机可能会设置目标线程的线程中断标记并将该线程唤醒,从而使目标线程被唤醒后继续执行的代码再次得到相应中断的机会。所以,给目标线程发送中断还能够产生唤醒目标线程的效果。
  48. 在单生产者-单消费者模式中,停止生产者、消费者线程有一种简单的方法:生产者线程在其终止前往传’输通道中存入一个特殊产品作为消费者线程的线程停止标记,消费者线程取出这个产品之后就可以退出run方法而终止了。

第六章 保障线程安全的设计技术