《Java并发编程的艺术》笔记

2022-10-23,,,,

第1章 并发编程的挑战
1.1 上下文切换
CPU通过时间片分配算法来循环执行任务,任务从保存到再加载的过程就是一次上下文切换。
减少上下文切换的方法有4种:无锁并发编程、CAS算法、使用最少线程、使用协程。
无锁并发编程:不同线程处理不同分片的数据,如数据哈希取模分片等。
CAS算法:java的Atomic包使用cas算法更新数据,不需要加锁。
使用最少线程:任务少时避免创建不需要的线程,否则大量线程会等待状态。
使用协程:在单线程里实现多任务的调度。
 
1.2 死锁
避免死锁的几个常用方法如下:
①避免一个线程同时获取多个锁。
②避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
③尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
④数据库锁的加锁和解锁必须在一个数据库连接里,否则会出现解锁失败。
 
1.3 资源限制的挑战
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源和软件资源。带来的问题为并发执行因为资源限制变成了串行执行,因为增加了上下文切换和调度时间,反而比串行还更慢。
硬件资源受限时,可以单机变成分布式多机执行;软件资源受限时,可以使用资源池将资源复用。
 
 
第2章 Java并发机制的底层实现原理
java的并发机制依赖于JVM的实现和CPU的指令。
 
2.1 volatile
volatile是轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”。它的执行成本比synchronized低,因为不会引起线程上下文的切换和调度。
可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
 
volatile的实现原则:
①Lock前缀指令会引起处理器缓存回写到内存。
最新处理器,Lock#指令会锁处理器缓存,而不锁总线;并使用缓存一致性机制来保证修改的原子性。
②一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
处理器使用嗅探技术来保证多个处理器缓存和内存的数据在总线上保持一致。
 
volatile的优化:通过追加字节,来使处理器的高速缓存行被锁定时,不会多锁定同一行内其他信息。
以下两种情况不应该追加字节来优化:缓存行非64字节宽的处理器;共享变量不会被频繁写。
 
2.2 synchronized
synchronized是重量级锁,实现的基础是java中每一个对象都可以做为锁,如:实例对象、class对象等。
synchronized在JVM中的实现原理:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。
monitorenter指令:在编译后插入到同步代码块的开始位置。
monitorexit指令:插入到方法结束处和异常处。
任何对象都有一个monitor对象与之关联,当一个线程获取到对象所对应的monitor的所有权,也就获取到该对象的锁。
 
(1)java对象头
synchronized用的锁是存放在java对象头里的。java对象头长度为2个字(非数组对象)或3个字(数组)。一个字为32bit(32位jvm)或64bit。这3个字依次存放信息如下:
①第一个字叫Mark Word,长度32/64bit,存储对象的hashcode、对象分代年龄、锁标志位等。
②第二个字叫Class Metadata Address,长度32/64bit,存储到对象类型元数据的指针。
③第三个字叫Array Length,长度32/64bit,存储数组的长度。
 
Mark Word里存储的数据会随着锁标志位的不同而变化,如图:

锁有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。为了提高锁竞争效率,锁只可以升级不能降级。
 
(2)偏向锁
经研究发现,大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低,而引入偏向锁。偏向锁使用了一种等到竞争出现才释放锁的机制。
A、偏向锁获取:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
B、偏向锁撤销:
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
 
(2)轻量级锁
A、轻量级锁获取:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
B、轻量级锁解锁:
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
 
(3)锁优缺点对比

 
2.3 原子操作
每个处理器都保证对内存的读写操作是原子性的。但多核处理器需要通过总线加锁或缓存加锁这两种机制来实现原子性。当以下两种情况不能使用缓存加锁:①操作数据不能被缓存在处理器内部,或数据跨多个缓存行。②有些处理器不支持缓存锁定。
java通过锁和循环CAS来实现原子操作,分别如下:
①循环CAS:存在3个问题:ABA问题、CPU开销大、只能保证一个共享变量的操作。
②锁:除了偏向锁采用if判断,其他锁在获取和退出时都使用了循环CAS。
 
 
第3章 Java内存模型
java的并发机制依赖于JVM的实现和CPU的指令。
3.1 JMM基础
并发编程模型要解决的两个关键问题:线程之间如何通信和线程之间如何同步。
通信:是指线程之间以何种机制来交换信息。常用有共享内存和消息传递两种,java并发采用共享内存模型。
同步:是指程序中用于控制不同线程间操作发生相对顺序的机制。
主内存:Main Memory,线程之间共享的变量保存在主内存。如所有实例域、静态域和数组元素。
本地内存:Local Memory,每个线程都有一个私有的本地内存,存储该线程读写共享变量的副本。如局部变量,方法实参,异常处理器参数等。本地内存是一个JMM的抽象概念,并不真实存在。
内存可见性保证:JMM通过控制主内存与每个线程的本地内存的交互,来给java程序员提供内存可见性保证。
 
重排序:是指编译器和处理器为了提高并行度来优化程序运行性能,对指令序列重排序。分为3类:编译器优化重排序、处理器指令并行重排序、处理器缓存重排序。
内存屏障指令:JMM在生成指令序列时,插入内存屏障指令来禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。分为四类:LoadLoad、StoreStore、LoadStore、StoreLoad(全能型,含前三种)。
处理器写缓冲区:临时保存向内存写入的数据,好处:批量写、合并相同地址的多次写、减少总线占用时间。
 
3.2 happens-before简介
happens-before是JMM中最核心的概念。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
happens-before的定义:
①如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。这是对程序员的承诺。
②两个操作存在happens-before关系,并不意味着java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序不影响执行结果,那重排序也是允许的。这是对编译器和处理器重排序的约束原则。
 
一个先行发生规则对应一条或多条编译器和处理器重排序规则,是提供给程序员的视图,为了避免程序员直接面对复杂的重排序规则,JMM则根据happens-before规则来禁止某种类型的编译器和处理器重排序规则,以提供内存可见性保证。
常见先行发生规则:
①程序顺序规则:一个线程的每个操作,先行发生于该线程的任意后续操作。
②监视器锁规则:对一个锁的解锁,先行发生于随后对这个锁的加锁。
③volatile变量规则:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读。
④传递性:如果A先行发生于B,B先行发生于C,则A先行发生于C。
 
数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作间就存在数据依赖性。不允许重排序。
as-if-serial语义:不管怎么重排序,单线程程序的执行结果不能被改变。
 
3.3 顺序一致性
顺序一致性内存模型是一个理论参考模型,在设计时,JMM和处理器内存模型都以它为参照。它为程序员提供了极强的内存可见性保证。
顺序一致性内存模型有两大特性:
①一个程序中的所有操作必须按照程序的顺序执行。
②所有线程都只能看到一个单一的操作执行顺序。每个操作都必须原子执行且立刻对所有线程可见。
 
3.4 volatile的内存语义
volatile变量的特性:
①可见性:对一个volatile变量的读,总是能看到对这个volatile变量最后的写入。
②原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种组合操作不具有原子性。
JMM会在volatile变量读写前后插入内存屏障。
 
volatile变量的写-读与锁的释放-获取有相同的内存效果。
volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
 
3.5 锁的内存语义
锁可以让临界区互斥执行。
锁释放内存语义:JMM会把本地内存中的共享变量刷新到主内存。
锁获取内存语义:JMM会把线程对应的本地内存置为无效,从而使得临界区代码必须从主内存中读取共享变量。
 
java的CAS同时具有volatile读和volatile写的内存语义。
volatile变量的读/写和CAS可以实现线程之间的通信,是JUC并发包实现的基石。
concurrent包的实现示意图分为上中下三层,分别如下:
①上层包括:Lock(同步器)、阻塞队列、Executor、并发容器。
②中层包括:AQS、非阻塞数据结构、原子变量类。
③下层包括:volatile变量的读/写,CAS。
 
3.6 final域的内存语义
final域的两个重排序规则:
①在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。(防止从构造函数“溢出”)
②初次读一个包含final域的对象的引用,与随后初次读这个final域,两个操作之间不能重排序。
 
3.7 双重检查锁定与延迟初始化
当采用双重检查锁定来实现延迟初始化时,可能会被重排序而失效。为了实现线程安全的延迟初始化,可以有两种方法如下:
①不允许重排序
把instance设为volatile类型,采用volatile变量自身的内存屏障。
②允许重排序,但不允许其他线程看到这个重排序
通过静态内部类的静态域初始化来实现。利用了JVM在class类被加载后,在被线程使用之前的初始化阶段会获取锁,来同步多个线程对同一个类的初始化的特性。

 
3.8 java内存模型概述
java程序内存可见性保证分为三类:
①单线程程序
不会出现内存可见性问题。
②正确同步的多线程程序
具有顺序一致性特性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同),JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
③未同步/未正确同步的多线程程序
JMM为他们提供最小安全性保证:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。
 
 
第4章 Java并发编程基础
4.1 线程简介
线程:是操作系统调度的最小单元,也叫轻量级进程(Light Weight Process)。
线程优先级:从1~10,默认5。优先级越高,则分到的CPU时间片资源越多。
线程状态:6种状态,分别为:NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED。

线程状态变迁图如下:

 
Daemon线程:一种支持性线程,主要被用做程序后台调度和支持性工作。当一个java虚拟机中不存在非Daemon线程的时候,java虚拟机将会退出。main线程不是daemon线程。
Thread.setDaemon(true)只能线程启动前设置,不能启动后设置。daemon线程的finally块不一定会执行。
 
4.2 启动和中断线程
线程对象在初始化完成后,调用start方法可以启动这个线程。
中断是线程的一个标识位属性,表示一个运行中的线程是否被其他线程调用该线程的interrupt方法进行过中断操作。线程可以通过调用isInterrupted方法来判断自身是否被中断。
中断标识位属性有两种复位方法:①Thread.interrupted()静态方法。②抛出InterruptedException中断异常。
安全的终止线程方法:通过标识位或中断操作来使线程在终止时有机会去清理资源。
 
suspend()、resume()和stop()方法会导致程序可能工作在不确定状态下,这些方法已作废,而暂停和恢复操作可以用等待/通知机制来替代。
 
4.3 线程间通信
(1)volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
 
(2)synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
下图描述了对象、对象的监视器、同步队列和执行线程之间的关系。

任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
 
(3)等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上。

 
等待/通知机制的经典范式:
A、等待方(消费者)遵循如下原则
①获得对象的锁。
②如果条件不满足,那么调用对象的wait方法,被通知后仍要检查条件。
③条件满足则执行相应的逻辑。

 
B、通知方(生产者)遵循如下原则
①获得对象的锁。
②改变条件。
③通知所有等待在对象上得线程。

 
(4)其他
管道输入/输出流是用于线程之间的数据传输,以内存为传输媒介。
主要包含4个类:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
 
thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
 
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
 
 
第5章 Java中的锁
5.1 Lock接口
锁:锁是用来控制多个线程访问共享资源的方式。
Lock接口:从JDK5开始增加了Lock接口,在这之前使用synchronized实行锁同步功能。
synchronized和Lock接口的区别:
①synchronized隐式的获取和释放锁,缺乏可操作性。
②Lock接口优点:显示获取和释放锁,可操作性、可中断的获取锁和超时获取锁。

 

 

Lock接口的常用实现是ReentrantLock,Lock接口的所有实现基本都是通过聚合了一个AQS同步器的子类来完成线程访问控制。
 
5.2 队列同步器
队列同步器:AbstractQueuedSynchronizer,简称同步器,是用来构建锁和其他同步组件的基础框架。使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排列工作。
队列同步器的主要使用方式是继承,子类通过继承同步器并实现他的抽象方法来管理同步状态。同步器的设计基于模板方法模式,模板方法基本分为三类:独占式获取与释放同步状态、共享式获取与释放同步状态、查询同步队列中的等待线程情况。
同步器是实现锁和任意同步组件的关键。在锁的实现中通过聚合同步器来实现锁的语义。锁面向使用者,而同步器面向锁的实现者。
同步器的作用:简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
(1)同步队列
队列同步器依赖其内部的同步队列(一个FIFO双向队列)来完成同步状态的管理。
原理:当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点Node,并通过循环CAS追加到同步队列的队尾,同时阻塞当前线程;当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。设置新首节点由原首节点里的线程完成,由于只有一个线程能成功获取到同步状态,故设置首节点不需要CAS。
首节点拥有同步状态,是成功获取到同步状态的节点。
(2)独占式同步状态获取与释放
独占式:同一时刻只能有一个线程获取到同步状态。
主要逻辑如下:

(3)共享式同步状态获取与释放
共享式:同一时刻能否有多个线程同时获取到同步状态。
共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞。
独占式访问资源时,同一时刻其他访问均被阻塞。
(4)独占式超时获取同步状态
响应中断的同步状态获取过程:在Java 5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在synchronized上,等待着获取锁。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。
超时获取同步状态过程:可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。
 
5.3 重入锁
重入锁:ReentrantLock,是一种排他锁,表示该锁能够支持一个线程对资源的重复加锁。另外还支持获取锁时的公平和非公平选择。
synchronized关键字隐式的支持重进入。不支持重进入的锁,当重复获取锁会把自己锁住。
重入锁实现原理:成功获取锁的线程再次获取锁,只是增加了同步状态值。要求重入锁在释放同步状态时减少同步状态值。当同步状态值为0,将占有线程设置为null,才表示释放成功。
公平锁:优点:保证了锁的获取按照FIFO原则,缺点:进行了大量的线程切换。
非公平锁:优点:极少线程切换,开销小,保证更大的吞吐量,缺点:可能造成线程“饥饿”。
 
5.4 读写锁
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
读写锁优点:保证写操作对后面读操作的可见性、并发性提升、简化读写交互场景的编程方式。
在JDK5读写锁出现之前,通过等待通知机制来实现,比较复杂。
ReentrantReadWriteLock是java并发包提供的读写锁实现。
ReentrantReadWriteLock的特性的特性如下:

锁降级:是指把持住当前拥有的写锁,再获取到读锁,随后释放先前拥有的写锁的过程。
锁降级目的:为了保证数据可见性。如果写锁释放后马上被另一个线程获取了写锁并修改了数据,那么其他线程可能无法感知到本次数据更新。
ReentrantReadWriteLock不支持锁升级,也是为了数据可见性。
 
同步状态:表示锁被一个线程重复获取的次数。用一个int整型表示。
读写锁的设计:将同步状态按位切分为两个部分,高16位表示读锁,低16位表示写锁。该状态的设计是读写锁实现的关键。
 
5.5 LockSupport工具类
LockSupport定义了一组公共静态方法(park阻塞和unpark唤醒),来提供最基本的线程阻塞和唤醒功能,是构建同步组件的基本工具。
 
5.6 Condition接口
Condition对象由Lock对象创建出来,当前线程要调用Condition接口的方法时,需要提前获取Condition对象关联的Lock对象锁。
等待/通知机制的两种实现方式:
(1)Object对象提供的监视器方法wait、notify、notifyAll等,这些方法与synchronized同步关键字配合。
(2)Condition接口提供的监视器方法await、signal等,这些方法与Lock配合。
 
实现了Condition接口的ConditionObject类是AQS同步器的内部类,每个Condition对象都包含一个FIFO等待队列,该等待队列是实现等待/通知功能的关键。
并发包中的Lock(更确切的说是同步器)拥有一个同步队列和多个等待队列。
等待的实现逻辑:当调用await方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
通知的实现逻辑:当调用signal方法时,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。当调用signalAll方法时,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
 
 
第6章 Java并发容器和框架
6.1 ConcurrentHashMap的实现原理与使用
hashmap:并发环境中hashmap线程不安全,put操作可能导致hashmap的Entry链表形成环状结构,一旦形成环状数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry,从而使CPU100%。
hashtable:因为使用synchronized关键字来保证线程安全,使得线程大量阻塞,导致效率非常低下。
ConcurrentHashMap锁分段技术:首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
 
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。segments数组的长度最大为65536。

(1)get操作非常高效,且不需要加锁,原因是它的get方法里将要使用的共享变量都定义成volatile类型,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作。
(2)put方法需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。
(3)size操作。ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
 
6.2 ConcurrentLinkedQueue
线程安全的队列有两种实现方式:
①阻塞方法:一个锁(入队和出队共用一把锁)或两个锁(入队和出队用不同的锁)。
②非阻塞方法:使用循环CAS。如ConcurrentLinkedQueue。
 
整个入队过程主要做两件事情:第一是定位出尾节点;第二是使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。
tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点。尾节点可能是tail节点,也可能是tail节点的next节点。
让tail节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑清晰和易懂。但是,这么做有个缺点,每次都需要使用循环CAS更新tail节点。
 
6.3 Java中的阻塞队列
阻塞队列:BlockingQueue,是一个支持阻塞插入和阻塞移除的队列。使用Condition接口和Lock的等待通知模式实现。
阻塞插入:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
阻塞移除:当队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

java提供了7个阻塞队列如下:
①ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。·
②LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。·
③PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。·
④DelayQueue:一个使用优先级队列实现的无界阻塞队列。·
⑤SynchronousQueue:一个不存储元素的阻塞队列。·
⑥LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。·
⑦LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
 
6.4 Fork/Join框架
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
 
 
第7章 Java中的12个原子操作类
tomic包里一共提供了12个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。Atomic包里的类基本都是使用Unsafe实现的包装类。
Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong。
7.1 原子更新基本类型类
AtomicBoolean:原子更新布尔类型。·
AtomicInteger:原子更新整型。·
AtomicLong:原子更新长整型。
 
再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。
 
7.2 原子更新数组类
通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类。·
AtomicIntegerArray:原子更新整型数组里的元素。·
AtomicLongArray:原子更新长整型数组里的元素。·
AtomicReferenceArray:原子更新引用类型数组里的元素。
 
7.3 原子更新引用类型
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。·
AtomicReference:原子更新引用类型。·
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。·
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。
 
7.4 原子更新字段类型
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新。·
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。·
AtomicLongFieldUpdater:原子更新长整型字段的更新器。·
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
 
 
第8章 Java中的并发工具类
CountDownLatch、CyclicBarrier、Semaphore工具类提供了一种并发流程控制的手段,而Exchanger工具类则提供了在线程间交换数据的一种手段。
8.1 CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
注意:调用CountDownLatch的await方法时不会阻塞当前线程。CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一个线程调用countDown方法happen-before另外一个线程调用await方法。
线程Join方法用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。
 
8.2 CyclicBarrier
CyclicBarrier:同步屏障,也是可循环使用(Cyclic)的屏障(Barrier)。让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。
 
8.3 Semaphore
Semaphore:信号量,用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
其构造方法Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。
 
8.4 Exchanger
Exchanger:交换者,用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
 
 
第9章 Java中的线程池
线程池的好处:降低资源消耗、提高响应速度、提高线程的可管理性。
9.1 线程池实现原理
线程池的主要处理流程图:

 
ThreadPoolExecutor执行示意图:

 
9.2 线程池的使用
(1)线程池创建
通过ThreadPoolExecutor来创建一个线程池。
new ThreadPoolExecutor(corePoolSize,maximumPoolSize, keepAliveTime,milliseconds,runnableTaskQueue, handler);
参数描述:
corePoolSize 线程池的基本大小;
maximumPoolSize 线程池最大数量;
keepAliveTime 线程活动保持时间;
milliseconds 线程活动保持时间的单位;
runnableTaskQueue 任务队列,用于保存等待执行任务的阻塞队列;可以选择ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue这4种。
handler 饱和策略,提供了以下4种策略(·AbortPolicy:直接抛出异常。·CallerRunsPolicy:只用调用者所在线程来运行任务。·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。·DiscardPolicy:不处理,丢弃掉)。
 
(2)向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
 
(3)关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
两个方法区别:shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。
 
(4)合理配置线程池
从以下几个角度来分析:·
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。·
任务的优先级:高、中和低。·
任务的执行时间:长、中和短。·
任务的依赖性:是否依赖其他系统资源,如数据库连接。
 
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
建议使用有界队列。
 
(5)线程池的监控
通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。
 
 
第10章 Executor框架
Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
10.1 Executor简介
在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
Executor框架的两级调度模型:在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。
 
Executor框架主要由3大部分组成:
①任务
包括被执行任务需要实现的接口:Runnable接口或Callable接口。
②任务的执行
包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
③异步计算的结果
包括接口Future和实现Future接口的FutureTask类。

 
10.2 ThreadPoolExecutor详解
Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类。ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。
FixedThreadPool被称为可重用固定线程数的线程池。

SingleThreadExecutor是使用单个worker线程的Executor。

CachedThreadPool是一个会根据需要创建新线程的线程池。SynchronousQueue是一个没有容量的阻塞队列。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

 

 
10.3 ScheduledThreadPoolExecutor详解
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。DelayQueue是一个无界队列。
ScheduledThreadPoolExecutor的执行主要分为两大部分:
1)当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWith-FixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
2)线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
 
10.4 FutureTask详解
Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask可以处于下面3种状态:未启动、已启动、已完成。
可以把FutureTask交给Executor执行;也可以通过ExecutorService.submit(...)方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.cancel(...)方法。除此以外,还可以单独使用FutureTask。
FutureTask的状态迁移示意图:

FutureTask的get和cancel的执行示意图:

 
FutureTask的实现基于AbstractQueuedSynchronizer(以下简称为AQS)。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。
基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。
 

《Java并发编程的艺术笔记的相关教程结束。

《《Java并发编程的艺术》笔记.doc》

下载本文的Word格式文档,以方便收藏与打印。