四万字长文解析 juc,涵盖线程、内存模型、锁、线程池、原子类、同步器、并发容器、并发编程模式、并发编程应用等。
版本:
JUC 是 java.util.concurrent 包的缩写,是 java 提供的用来并发编程的工具包。juc 提供了多种用于多线程编程金额并发控制的接口和类。juc 主要包括以下五大类组件:
二者区别与联系:
操作系统层面定义的线程有以下五个状态:
Java 语言层面定义的线程有以下六种状态:
各状态见的连线表示线程状态转换,请移步 3.3.7。
Java 中定义了 Thread 类来表示线程,其内部枚举类 Thread.State 维护了线程的六种状态。线程的创建共有以下三种方式。
Thread 类为 Java API 中用来描述线程的,其类图如上所示。其用法如下:
// 创建线程对象
Thread thread = new Thread() {
@Override
public void run() {
log.info("one")
}
}
// 调用其 start() 方法启动线程
thread.start();
Runnable 接口是 juc 包提供的任务接口,其定义了 run() 方法,用来运行具体任务。通过其创建的任务可交由 Thread 线程对象去执行。其用法如下:
// 创建任务对象(通过函数式接口)
Runnable task = () -> {
log.info("two");
};
// 交由线程去执行
Thread thread = new Thread(task);
thread.start();
Callable< V> 是 juc 包提供的有返回值的任务接口,由其创建的任务可交由 Thread 线程去执行。其执行接口可通过 FutureTask< V> 来接收,其中范型 V 表示任务执行的结果类型。其用法如下:
// 创建任务对象(通过函数式接口创建) 任务执行结果为 String 类型
Callable<String> task = () -> {
return "three";
};
// 创建执行结果接收对象
FutureTask<String> future = new FutureTask<>(task);
// 将任务交由线程去执行
Thread thread = new Thread(future);
thread.start();
// 通过 FutureTask 实例的 get() 获取任务执行结果
log.info("{}", future.get());
线程上下文切换是指由于某些原因导致 CPU 不在执行当前线程,而去执行另外的线程。造成线程上下文切换的原因有以下几种:
发生线程上下文切换时,需要保存当前线程的状态,并恢复另一个要执行的线程的状态,对应到 java 中的概念则为程序计数器和虚拟机栈中的每个栈帧的信息。程序计数器的作用记住下一条要执行的 jvm 命令的地址,栈帧则表示了当前线程所执行的方法涉及到的局部变量、操作数栈、返回地址等。
注:频繁发生上下文切换会影响程序性能。
由于 CPU 的计算速度非常快,而内存的访问速度相对而言较慢,若 CPU 每次都从内存读取数据会造成 CPU 等待,降低 CPU 利用率,所以,在 CPU 与内存之间引入了高速缓存,来减少内存访问次数,从而提高整体性能。
CPU 缓存及其物理实现如上图所示,缓存分为三级分别是:一级缓存、二级缓存、三级缓存,且其访问速度依次递减,其大小依次递增,L1、L2 为各 CPU 私有,L3为各 CPU 共享。
下图为 CPU 访问各存储介质所需时间(时钟周期):
存储介质 | 所需时钟周期 |
---|---|
寄存器 | 1 cycle (4 GHz 的 CPU 约为 0.25ns) |
一级缓存 | 3 ~ 4 cycle |
二级缓存 | 10 ~ 20 cycle |
三级缓存 | 40 ~ 45 cycle |
内存 | 120 ~ 240 cycle |
CPU 缓存的最小读写单元为缓存行(Cache Line),且其从内存往缓存读取数据是一小块一小块读取的,每一块对应一个缓存行,缓存行大小一般为 64 byte。CPU 读取数据时会按照一级缓存、二级缓存、三级缓存、内存顺序依次读取,即从高级缓存到低级缓存。缓存行结构如下:
上图为 CPU 缓存数据的读写策略,即缓存数据读写原则。当缓存行中的数据更新后,同时需要将该缓存行标记为脏数据,表示该缓存行数据已被修改,当该缓存行下次被使用时,若为脏,则需要将缓存行中数据写入低级存储中。即这里的 “脏” 并非表示该缓存中数据被抛弃,而是说该缓存行中数据可能已被修改需要同步到低级存储中。简单来说就是脏表示缓存行中数据已被修改,非脏表示未被修改。
从上面的读写原则可以看出,CPU 在对缓存进行读写时采取 “就近” 原则,即读的时候从离它最近(物理最近)的那个缓存开始,若读到了就返回,没读到就读下一个;写亦是,只写到离它最近的缓存,而不是一直写到内存。
CPU 与内存之间引入高速缓存后会造成缓存一致性问题,因为原则上多个 CPU 共享一份数据,但每个 CPU 在更新数据时只更新到缓存中,这就会造成其它 CPU 看不到更新后的数据,即每个 CPU 对应缓存中的数据不一致。针对该问题,有以下几种解决方案。
总线嗅探:
即通过 CPU 总线来传播读写请求,各 CPU 通过监听总线上的请求来对自己缓存中各缓存行的状态作出修改。
当某个 CPU 执行一个写操作时,会在总线上广播一个写请求,其它 CPU 通过总线嗅探器监听到该写请求,然后将请求中的地址(要更新的数据在内存中的地址)与自己各缓存行中的数据地址(tag)进行映射,若映射到了某个缓存行,则将其 标记为无效,表示该数据已过期,下次读取该数据时需要从主存或其它级别的缓存中重新加载。
当某个 CPU 执行一个读操作时,会在总线上广播一个读请求,其它 CPU 通过总线嗅探器监听到该读请求,若能映射到某个缓存行,则判该缓存行数据是否为脏,若为脏则说明该数据已被该 CPU 修改,则总线嗅探器负责将该缓存行中数据更新到主存或其它级别缓存中。
事务串行化:
即将对同一共享数据的读写操作串行化,也就是把对同一共享数据的并发操作串行化,以保证数据的有效性。其串行是以各个 CPU 为单位。
MESI:
事务串行化时,每当有 CPU 修改数据,都需要在总线上广播给其它 CPU,但并不是所有 CPU 都用到这个数据,这样就会浪费资源。于是就引入了 MESI 来解决这个问题。
MESI 是一种缓存一致性协议,用于解决多核 CPU 的缓存一致性问题。MESI 代表四种状态,分别是:修改、独占、共享、无效。CPU 中所有缓存行的状态都用 MESI 协议标记。
注:不同 CPU 具体实现中用了不同的协议,但都大同小异。
内存屏障是一种 CPU 指令,用来解决特定条件下指令重排和内存可见性问题。内存屏障分为读屏障和写屏障。
为了提高 CPU 性能,引入了多级缓存,从而产生了缓存一致性问题,接着通过缓存一致性协议解决了缓存一致性问题。Java 为了解决多平台运行问题,当然也要解决缓存一致性问题,于是进一步抽象得到了 java 语言层面的内存模型。
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、高速缓存、内存、CPU 指令优化等。java 内存模型分为主内存和工作内存,且规定了所有变量都必须存储在主存中,工作内存为线程私有,即每个线程都有自己的工作内存,工作内存中存储的数据都是该线程工作是所需变脸在主存中的拷贝(即把自己需要的数据复制一份放到工作内存中)。线程对变量的所有操作都必须在工作内存中操作,而不能直接操作主存。不同线程之间无法访问对方工作内存中的变量,且线程间的变量传递都是在工作内存和主存间进行的。
JMM 具有原子性、可见性、有序性三大特性,同时,其针对主存和工作内存之间的交互还定义了八大基本原子操作和八大操作原则。(其目的是为了保证并发编程下数据的准确性)。
乐观锁与悲观锁是一种广义上的概念,体现了看待线程的不同角度。在 java 和数据库中都有对此概念的实际应用。
乐观锁:
乐观锁认为自己在使用数据时不会有其它线程来修改数据,所以在使用数据时不会加锁,只有在更新数据时才会判断该数据是否已被修改,若未被修改则成功写入,若该数据已被其它线程修改,则根据不同的实现方式执行不同的操作(如报错或重试)。
乐观锁适合读操作多的场景,不加锁可使读操作的性能大幅提升。乐观锁在 java 中是通过无锁编程来实现的,最常采用的是 CAS 算法,如原子类等。
悲观锁:
悲观锁认为自己在使用数据时一定会有其它线程来修改数据,所以在使用数据时会先加锁,确保在使用过程中数据不会被其它线程修改。
悲观锁适合写操作多的场景,加锁可保证写操作时的数据准确性。悲观锁在 java 中的具体表现为 synchronized 关键字和 Lock 接口的部分实现类等。
自旋锁:
阻塞或唤醒一个线程需要进行上下文切换,上下文切换会消耗处理器时间。如果同步代码块的逻辑较简单,则上下文切换所花费的时间可能比同步代码块的执行时间还要长。即,在很多场景下,同步资源锁定时间很短,为了这一小段时间而让线程阻塞或唤醒可能会让整个操作得不偿失,在这种情况下可以让获取锁失败的线程不阻塞(即不放弃 CPU 时间片),一直尝试获取锁,这就是自旋锁。
自旋本身是有缺点的,它不能代替阻塞。自旋的目的是避免线程切换带来的开销,但它会占用 CPU 时间。若自旋时间很短,则其效果很好;若其自旋时间很长,那线程就会一直占用 CPU 资源。所以自旋必须要有时间限制(默认是 10 次,可通过参数 XX:PreBlockSpin 参数更改),如果超过自旋次数,那么线程就应该挂起,释放 CPU 时间片。
适应性自旋锁:
jdk-1.6 中默认开启了自旋锁,并引入了适应性自旋锁。适应性自旋锁的自旋次数将不再固定,而是由前一次在同一个锁上的自旋时间和该锁的拥有者的状态决定的。若前一次某个线程通过自旋成功获取到锁,且该线程正在运行中,那么虚拟机就认为这次通过自旋也很有可能会获取到锁,进而它会允许这次自旋时间比前一次稍长点。若一个锁,通过自旋很少成功获取到,那么在以后尝试获取该锁的过程中将可能省略掉自旋这个过程,直接阻塞线程,避免消耗 CPU 资源。
自旋锁在 java 中的具体表现为 TicketLock、CLHLock、MCSLock 类和 synchronized 关键字等。
公平锁:
公平锁是指竞争锁的多个线程按照竞争顺序来获取锁。线程直接进入队列中排队,队列中的第一个线程将会获得锁。
公平锁的优点是等待锁的线程不会饿死;缺点是整体吞吐率相对非公平锁较低,除队列中第一个线程外其它线程都会阻塞,CPU 唤醒线程的开销要比非公平大。
非公平锁:
非公平锁是指线程竞争锁时会先插队尝试获取锁,若未获取到才会进入等待队列。假如某个线程竞争某个锁时该锁刚好被释放,那么该线程将不用阻塞直接获取到锁,所以非公平锁会出现后竞争但先获取到锁的场景。
非公平锁的优点是可以降低 CPU 唤醒线程带来的开销,整体吞吐率较高,因为线程有几率不阻塞直接获取到锁;缺点是处于等待队列的线程可能很久才能获取到锁甚至饿死。
公平锁和非公平锁在 java 中的具体表现为 ReentrantLock 等。
共享锁和排它锁(独享锁)同样是一种概念。
共享锁:
共享锁是指该锁可以被多个线程同时持有。若某个线程对某个数据加上共享锁后,其它线程就只能对该数据加共享锁了,不能加独享锁。获得共享锁的线程只能读取数据,不能修改数据。
独享锁:
独享锁也称排它锁,是指锁同一时刻只能被一个线程持有。若某个线程对某个数据加上独享锁后,其它线程就不能对该数据加任何锁了。获得独享锁的线程既可以读取数据也可以修改数据。
独享锁在 java 中的具体表现为 synchronized 关键字和 Lock 接口的部分实现类(如 ReentrantReadWriteLock)等。
可重入锁:
可重入锁是指同一个线程在外层方法获取到锁,在进入到该线程的内层方法后会自动获取到锁(前提是同一个锁),不会因为之前已经获取过但没释放而阻塞。
可重入锁的一个优点是可以一定程度上避免死锁。可重入锁在 java 中的表现为 synchronized 关键字和 Lock 的部分实现类(如 ReentrantLock)等。
不可重入锁
不可重入锁是指不管是否同一个锁还是同一个线程亦或是方法内外层,获取后只能先释放再获取。
不可重入锁在 java 中的表现为 NoReentrantLock 等。
锁升级是指在多线程竞争锁时对象锁状态的变化过程,升级流程:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 。
无锁:
无锁是指对同步资源没有锁定,所有线程都可并发修改同步资源,但同时只有一个线程可以成功。
无锁的特点是修改操作在循环内进行,线程会不断尝试修改同步资源,若没有冲突则成功修改并退出(这里的冲突是线程工作内存中该变量的值与主存中该变量的值是否相等);若有冲突则说明有多个线程在并发修改,此时,只有一个线程会成功,其它失败的线程将会不断重试直至成功。
无锁一般使用 CAS 算法实现。无锁无法全面代替有锁,但无锁在某些场景下性能是非常高的。
偏向锁:
偏向锁是指同步代码块一直被同一个线程执行,那么该线程会自动获取到锁,以此来降低获取锁的代价。
在大多数情况下锁总是由同一线程多次获得,很少会出现竞争的情况,所以就出现了偏向锁。
偏向锁在 jdk-1.6 及以后版本中是默认开启的,可通过参数 -XX:UseBiasedlocking=false 来关闭,关闭后程序会默认进入轻量级锁。
轻量级锁:
轻量级锁是指当锁为偏向锁时,有其它线程来尝试获取锁,此时锁会升级为轻量级锁。其它线程会通过自旋的方式等待,不会阻塞,从而提高性能。
重量级锁:
当锁是轻量级锁时,若当前只有一个线程在等待锁,那么该线程将会通过自旋的方式等待;但当自旋超过一定次数或当有第三个线程来竞争锁时,此时锁会升级为重量级锁。锁升级为重量级锁后,所有等待锁的线程都会进入阻塞状态。
java 中锁的实现可分为锁关键字和锁 API 两种。
volatile 底层原理是使用了内存屏障。对被 volatile 修饰的变量的写指令后加入了写屏障,对被 volatile 修饰的变量的读指令前加入了读屏障。
volatile 与可见性:
被 volatile 修饰的变量在读写指令前后加入了读写屏障,读写屏障可以保证共享变量在多线程见的可见性。
读屏障的作用是在该屏障之后对变量的读取都从主存中加载,这样可以保证共享该变量的线程每次读取到的都是主存中的最新值。
写屏障的作用是在该屏障之前对变量的修改都会同步到主存中,这样可以保证共享该变量的线程每次更新值后都会立刻同步到主存中。
在 jvm 层面,修改变量时,jvm 会向处理器发送一条 lock 前缀的指令,这条指令的作用是将修改后的值同步到内存而不是只写到缓存,同时由于 CPU 缓存一致性协议,其它 CPU 会监听到总线上的消息,然后将自己缓存中对应变量的状态置为无效。
volatile 与有序性:
被 volatile 修饰的变量在读写指令前后加入了读写屏障,读写屏障可以保证对共享变量操作时代码的有序性。可以简单理解为读写屏障禁止了指令重排。
读屏障可以保证在指令重排时,不会将读屏障之后的代码重排到读屏障之前。
写屏障可以保证在 指令重排时,不会将写屏障之前的代码重排到写屏障之后。
volatile 与原子性:
被 volatile 修饰的变量在读写指令前后虽然加入了读写屏障,但其并不能保证其原子性。
因为虽然读写屏障禁止了指令重排,但写屏障并不能阻止读指令跑到写屏障前面执行;同样的读屏障也不能保证写指令跑到读屏障后面。换言之,读写屏障并不能阻止读写指令的交错执行。
CAS 全称是 Compare And Swap,即比较与替换,是一种无锁算法。在不阻塞线程的情况下可以实现多线程间的变量同步。juc 包中的原子类即用 CAS 实现,以及 AQS 中锁状态的改变也是使用 CAS 实现。
CAS 算法设计到三个操作数:
CAS 算法的原理是,比较 A 与 V 的值是否相等,若相等则将 V 替换成 B,若不相等则继续重复比较替换。换言之,其会比较当前线程读取到的该变量的值愈该变量在内存中的值是否相等(因为在该线程读取了该变量在内存中的值后,该值可能会被其它线程修改,所以要比较),若二者相等,则将该变量在内存中的值(旧值 V)用修改后的值(新值 B)替换掉,且整个比较替换过程是在循环中进行的,若替换成功则会退出循环。比较和替换是一个原子操作。
CAS 存在的三大问题:
ABA 问题:
即当当前线程读取到该变量的值为 A 后,该变量在内存中的值被其它线程由 A 修改为 B,再被修改成 A,这样当当前线程进行 CAS 操作时虽然能成功,但实际上该变量的值是被修改过的。解决思路是给变量加上版本好,每次修改时都将版本加 1,这样 ABA 过程将变成 1A -> 2B -> 3A。
jdk 1.5 中增加了 AtomicStampedReference 类来解决 ABA 问题,具体使用 compareAndAset(),其会先检查变量的当前引用和当前版本与预期引用和预期版本是否相等,若相等则将引用值和版本值用给定的更新值替换。
循环时间长开销大问题:
CAS 若长时间不成功会一直循环,会长时间占用 CPU 资源。
只能保证一个共享变量的原子操作问题:
CAS 只能保证一个共享变量的操作问题,对多变量的操作是不支持的。
jdk 1.5 中增加了 AtomicReference 类来保证引用对象的原子操作,可以将多个共享变量放在一个对象中来保证原子操作。
CAS 算法在 java 中具体实现是由 Unsafe 类提供的。在执行 cas 函数时会向 CPU 发送一条 lock 前缀的指令,然后 CPU 会锁住总线,待当前核执行完指令后才会开启总线,且此过程不会被线程的打断机制打断。
juc 中 CAS 的常用方式是 CAS 结合 while、do while 语句来使用,如下所示:
// while 即只有当 CAS 成功后才返回
while (true) {
// todo
if (compareAndSet(expect, update)) {
return;
}
}
// do while
do {
// todo
if (compareAndSet(expect, update)) {
return;
}
} while (true);
注:需要用 CAS 操作的变量必须被 volatile 修饰。
对象头结构:
32 位虚拟机下 Mark Word:
64 位虚拟机下 Mark Word:
和 32 位虚拟机下的 Mark Word 一样。
Monitor,即监视器或管程,它是一个术语,指的是进程同步。Monitor 在 jvm 中由 C++ 的 ObjectMonitor 实现,ObjectMonitor 通过 enter、exit、wait、notify、notifyAll 等方法来实现加锁、解锁、等待、唤醒等。
在 java 中,每个对象都可以关联一个 Monitor 对象,当使用 synchronized 关键字给对象上锁后(重量级锁),该对象对象头中的 Mark Word 部分就会被设置为指向 Monitor 对象的指针。Monitor 结构如下:
测试代码:
public class SynchronizedTest {
public static final Object lock = new Object();
public static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
其主要字节码如下:
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // 加载 lock 对象引用
3: dup // 将引用复制一份
4: astore_1 // 将引用存储
5: monitorenter // 加锁 对应 ObjectMonitor 中的 enter 方法
6: getstatic #3 // 6 ~ 14 为临界区代码对应的字节码指令
9: iconst_1
10: iadd
11: putstatic #3
14: aload_1
15: monitorexit // 解锁 对应 ObjectMonitor 中的 exit 方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 解锁 对应 ObjectMonitor 中的 exit 方法
22: aload_2
23: athrow
24: return
Exception table: // 异常表
from to target type
6 16 19 any
19 22 19 any
什么?你说无锁原理,无锁有什么原理,无锁就是无锁,无锁就是 Mark Word 后三位为 001。
实现原理是 CAS 和 volatile。首先 CAS 只是一种算法,即比较和替换,如果相等则替换,否则就失败;其次,并发情况下必然是多个线程同时访问共享变量,又因为 CPU 多级缓存的存在,导致共享变量会出现可见性问题(共享变量可见性问题本质上是多核 CPU 的私有缓存造成的),于是就需要保证共享变量的可见性,这时候 volatile 就派上用场了;最后,再结合 while (true) 之类的死循环就实现了无锁。所以从本质上来说,CAS、volatile、while (true) 是无锁的三大件。换言之,CAS 操作必须借助 volatile 来获取到变量最新值以达到比较替换的效果。
偏向锁状态:
偏向锁默认是开启的,当同步代码块一直被同一个线程执行时,会使用偏向锁。该锁对象对象头中 Mark Word 中的 biased_lock 将被置为 1,即 Mark Word 后三位为 101 时,表示当前处于偏向锁状态,此时 thread 为锁偏向线程的线程 ID。
// 偏向锁时锁对象对象头将长这个样子
thread: 54 epoch:2 unused: 1 age: 4 biased_lock: 1 01
偏向锁开启后,默认是有延迟的,不会在程序启动后立即生效,可通过参数 -XX:BiasedLockingStartupDelay=0 来设置延迟时间,0 表示无延迟。
若未开启偏向锁,则当只有一个线程访问同步代码块时锁状态为轻量级锁,当多个线程竞争时锁会膨胀为重量级锁。
偏向锁撤销:
调用 hashCode() 方法会撤销偏向锁:
当锁状态为偏向锁时,调用锁对象的 hashCode() 方法会撤销偏向锁,锁升级为轻量级锁。
// 无锁时 Mark Word 长这样 共使用 38 位
unused: 25 hashcode: 31 unused: 1 age: 4 biased_lock: 0 01
// 偏向锁时 Mark Word 长这样 共使用 61 位
thread: 54 epoch: 2 unused: 1 age: 4 biased_lock: 1 01
// 偏向锁时 共占用 61 位 此时若再存储 hashCode 61 + 31 > 64 放不下了 所以会撤销偏向锁 升级为轻量级锁
// 轻量级锁时 Mark Word 长这样 其中 ptr_to_lock_recor 表示 Lock Record 锁记录在栈中的指针
ptr_to_lock_record: 62 00
第二个线程竞争锁时会撤销偏向锁:
当锁状态为偏向锁时,第二个线程来竞争锁,会撤销偏向锁,锁升级为轻量级锁。此时第二个线程将通过自旋方式来尝试获取锁,并不会阻塞。
调用 wait()、notify() 方法会撤销偏向锁:
当锁状态为偏向锁时,若调用锁对象的 wait() 方法,再从线程中调用锁对象的 notify() 方法,会撤销偏向锁,锁升级为重量级锁。
// 重量级锁时 Mark Word 长这样 其中 ptr_to_heavyweight_monitor 表示 Monitor 对象的指针
ptr_to_heavyweight_monitor: 62 10
批量重偏向:
当锁状态为偏向锁时,多个线程访问同步代码块,但没有竞争,这时,Mark Word 中的 thread 会从上一个使用锁对象的线程 id 替换为当前将要使用锁的线程 id,当这种替换超过 20 次后,jvm 会将剩余锁对象的线程 id 偏向至当前线程。
1、如有 30 个锁对象 都放置该集合 list 中
2、线程 1 先使用这 30 个对象(此时无其它线程竞争)
3、线程 1 使用完后 这 30 个锁对象都将偏向线程 1
4、线程 1 结束后线程 2 使用这 30 个锁对象
5、线程 2 使用时 其中前 19 个锁对象对象头中的 thread 将被替换为线程 2 的 id
6、从第 20 个开始 jvm 会批量将剩余锁对象对象头中的 thread 主动替换为线程 2 的 id
批量撤销:
当某个类型的锁对象的偏向锁撤销次数超过 40 次后,jvm 会将该类型对应的剩余锁对象的偏向锁撤销,使其升级为轻量级锁,同时,新建的该类型的锁对象也会禁止偏向,直接从轻量级锁开始。
当锁状态为偏向锁时,若有第二个线程来竞争锁,此时锁将升级为轻量级锁。第二个线程将通过 CAS 的方式来尝试获取锁,此时第二个线程是不阻塞的。若第二个线程通过 CAS 能获取到锁,则锁升级为轻量级锁;CAS 尝试一定次数后仍获取不到锁,则锁将升级为重量级锁。
第二个线程获取锁时(假设第二个线程的线程名为 Thread-1),线程与锁对象的结构如下:
首先,其会在线程栈上创建一个 Lock Record(锁记录),锁记录用来存储锁对象相关信息。
lock_object reference:表示锁对象的引用。
ptr_to_lock_record:表示锁记录地址。
其次,其会用 CAS 的方式替换 Lock Record 中 ptr_to_lock_record 与 Lock Object 对象头中的 Mark Word,若替换成功则表示加锁成功,此时锁为轻量级锁。若替换失败,则有以下两种情况:
若 CAS 替换成功,则线程与锁对象结构如下:
若出现锁重入,则线程与锁对象结构如下:
轻量级锁解锁时,若 Lock Record 的值不为 null,则会使用 CAS 尝试将 Mark Word 与 ptr_to_lock_record 恢复。若成功,则解锁成功;若失败,则说明锁已升级为重量级锁,此时将进入重量级锁解锁流程。
当锁为轻量级锁时有其它线程来竞争锁,或锁升级为轻量级锁过程中 CAS 失败,此时锁会升级为重量级锁。重量级锁时线程和锁对象的结构如下:
wait()、wait(long n)、notify()、notifyAll() 都属于 Object 对象方法,调用这些方法的前提是先获取到当前对象锁。其需配合 synchronized 使用,即底层依赖于 Monitor 实现。其含义如下:
wait、notify 特点:
sleep(long n) 与 wait(long n):
park 和 unpark 是 LockSupport 类中的方法,先 park 再 unpark。其底层依赖于 Unsafe 类实现,实际上调用的是 Unsafe 类的 park 和 unpark 方法。其含义如下:
wait & notify 与 park & unpark:
原理:
每一个线程都有一个属于自己的 Parker 对象,Parker 由 counter、cond、mutex 三部分组成,分别表示计数器、条件变量、锁。
其中 counter 有 0 和 1 两种状态。
park():
若 counter 为 0,则当前线程等待在条件变量 cond 上。
若 counter 为 1,则当前线程继续运行,并将 counter 置为 0.
unpark():
若当前线程正在等待,则唤醒等待在条件变量上的线程,并将 counter 置为 1。
若当前线程正在运行,则将 counter 置为 1。
先 unpark() 再 park():
先将 counter 置为 1,然后 park(),此时 counter 为 1 线程会继续运行,并将 counter 置为 0。
join 是 Thread 对象方法,其效果是在 A 线程中调用 B 线程的 join 方法,此时,A 线程会等待 B 线程运行结束才继续运行。
其实现原理是 A 线程轮询检查 B 线程是否存活,若 B 存活则 A 进入 B 线程对象对应的 Monitor 的 WaitSet 集合中等待;若 B 已运行结束则 A 继续运行。等价于以下代码:
Thread b = new Thread(() -> {
// 业务代码
});
Thread a = new Thread(() -> {
// 业务代码
// 等待 b 线程运行结束
// 若 b 未结束 则 a 会进入 b 对象对应的 Monitor 的 WaitSet 集合中等待
// 若 b 结束 则 a 被唤醒 继续执行
synchronized(b) {
while(b.isAlive()) {
b.wait(0);
}
}
// 业务代码
});
假设有线程 Thread t,其六种状态间的转换如下:
AQS 即 AbstractQueuedSynchronizer 抽象队列同步器。它是 juc 包中提供的实现锁或同步器的基础组件。
java 中有两大类锁,一种是内置的关键字锁,如 synchronized、volatile 等;另一种是 Lock 接口的各种实现类锁,如 ReentrantLock、ReentrantReadWriteLock 等。其中前者 synchronized 关键字的实现依赖于 Monitor 类,而后者的实现则依赖于 AQS。
AQS 提供了独占模式和共享模式两种锁的获取,同时又实现了阻塞式锁和非阻塞式锁。
AQS 在功能上类似于 Monitor 却又更加灵活于 Monitor。同时其在结构上也与 Monitor 较为相似。
state:
state 用来表示同步状态或锁状态。其是 AQS 维护的一个用 volatile 修饰的 int 变量。类似于 Monitor 中的 count。
AQS 本质上则是多个线程在并发情况下通过感知和修改 state 的值来达到加锁和解锁的目的。所以其用 volatile 修饰,以保证在多线程间可见;同时在修改时使用 CAS 算法,一定程度上可提升性能。
state 是抽象的,其在不同锁的实现中代表着不同的含义。如:
CLH 队列:
CLH 即由 Craig、Landin、Hagersten 三人同时发明,所以叫 CLH。它是个单向链表,AQS 中的 CLH 是其变体的双向链表且 FIFO (先进先出)。类似于 Monitor 中的 EntryList 集合。
CLH 在 AQS 中用来存放竞争锁失败而阻塞的线程。其节点由 AQS 内部类 Node 表示,Node 类内部持有了当前被阻塞的线程对象,同时持有了当前节点的钱去节点 prev 和后记节点 next,还维护了节点状态。
其中 AQS 中维护了 CLH 队列的头节点 head 和尾节点 tail,且二者用 volatile 修饰,同时修改头节点和尾节点时用 CAS 算法。
优点:
ConditionObject:
ConditionObject 表示条件变量,其功能类似于 Monitor 中的 wait()、notify() 的作用对象。由内部的 await()、signal() 方法实现等待和唤醒操作。
同时其维护了一个以 Node 对象为为节点的单向链表,用来表示正在等待的线程。类似于 Monitor 中的 WaitSet 集合。其中 firstWaiter 表示头节点,lastWaiter 表示尾节点,修改头尾节点时用 CAS 算法。
与 WaitSet 不同的是,AQS 的等待可以等待在多个条件变量上(每个 ConditionObject 都拥有一个单向链表),而 synchronized 锁的等待只能等待在同一个锁对象上。
AQS 是通过 LockSupport 的 park、unpark 函数来实现线程的阻塞和唤醒的。
AQS 作为 juc 中最基本、最重要且最底层的组件,其已经实现了大部分相关功能的实现。如:
同时,AQS 又实现了时间相同的功能,如在指定时间内获取锁等。而对于 AQS 的使用,我们只需要继承它,然后实现其需要由子类实现的四个抽象方法即可。在抽象方法的具体实现中可通过调用 AQS 已实现的相关功能来实现自定义同步器的具体功能。
注:AQS 已经实现了阻塞锁获取和释放的相关功能,而未实现非阻塞锁获取和释放的相关功能,是因为阻塞锁的获取和释放需要操作 CLH 队列 和 等待队列,其操作过程较为复杂且通用。
Node 类是 AQS 中定义的内部类,表示 CLH 队列和等待队列中的节点。当线程竞争锁失败或调用条件变量的 await() 方法时会被封装成 Node 对象入然后入队。其主要属性如下:
waitStatus 值及说明如下:
此外,Node 类内部还维护了两个 Node 属性,用来表示节点的共享和独占状态,其分别是:
注:在 CLH 队列中,第一个节点(头节点 head)不关联任何线程,即其 thread 属性永远为 null,头节点只是作为哨兵节点存在。
synchronized 锁配合锁对象(Object 对象)的 wait、notify 类函数实现了线程间的同步协作。AQS 也实现了类似的功能。AQS 中的内部类 ConditionObject(条件变量)提供了 await、signal 类函数,其作用类似于 Object 的 wait、notify 类函数;ConditionObject 类中维护的单向链表,类似于 synchronized 锁对象对应 Monitor 对象中的 WaitSet 集合,都充当等待队列的角色。
二者之间的区别是:
Lock 接口是 juc 包中提供的定义了锁相关行为的接口,其结合 AQS 实现了比 synchronized 隐士锁更加灵活、扩展性更强。其定义的主要函数如下:
Lock 接口主要实现类有:
ReentrantLock 是 java 中最常用的一种锁实现。其是独占的、可重入的、阻塞或非阻塞的、公平或非公平的。
ReentrantLock 类图如上图所示,其实现了 Lock 接口(Lock 接口见 3.4.1),Lock 接口中定义了阻塞式锁和非阻塞式锁的获取函数及锁释放函数。ReentrantLock 类定义了内部类 Sync 同步器,继承自 AQS,实现了 AQS 中的 tryRelease、isHeldExclusively 函数。同时又为 Sync 同步器扩展了两个子类 FairSync、NonfairSync,分别表示公平锁、非公平锁的实现,它们都实现了 AQS 的 tryAcquire 函数。
ReentrantLock 初始化时会初始化 Sync 实例,默认会初始化 NonfairSync,即 ReentrnatLock 默认为非公平锁。
ReentrantLock#lock()
阻塞锁的获取从 ReentrantLock 的 lock() 方法开始,lock() 方法内部直接调用了 AQS 的 acquire() 方法。大致流程如下:
ReentrantLock#Sync#tryAcquire()
// 公平实现
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 非公平实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上图所示为 ReentrantLock 的内部类 Sync 的两个子类实现的 AQS 类中定义的抽象方法 tryAcquire() 的流程。tryAcquire() 意为尝试获取锁,这里的尝试可理解为不会阻塞线程。该函数返回一个 boolean,true 表示获取锁成功,false 表示失败。
左侧为公平锁的同步器 FairSync 的实现,其大致流程如下:
右侧为非公平锁的同步器 NonfairSync 的实现,其与公平同步器的实现流程大致一样,唯一区别是在第 3 步判断 state 等于 0 后会直接略过第 4 步走第 5 步。简言之,公平锁会判断阻塞队列中是否有正在等待的线程,而非公平锁则不会判断,上来就抢。
AQS#acquireQueued()
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
如图所示,其为 AQS 实现的阻塞锁的获取逻辑。即当线程获取锁失败需要通过阻塞来等待锁的释放,然后重新获取。该函数返回一个 boolean 值,true 表示该线程在阻塞等待锁的过程中被其它线程打断过,false 表示该线程在阻塞等待锁的过程中没有被其它线程打断过。
实际上在调用该函数前,会先调用 AQS 的 addWaiter() 函数,此函数的作用是将当前线程封装成 Node 实例,并设置 waitStatus 的值为 0,然后将其添加到阻塞队列(CLH)的队尾。且封装的 Node 实例将作为 acquireQueued() 函数的入参。
acquireQueued() 函数大致流程如下:
ReentrantLock#unlock()
Lock 锁的释放都从 unlock() 函数开始,所以 ReentrantLock 也是。而 ReentrantLock 实现的 unlock() 函数则是在内部直接调用了 AQS 的 release() 函数。其大致流程如下:
ReentrantLock#Sync#tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如图所示,其为 ReentrantLock 内部类 Sync 实现的 AQS 定义的 tryRelease() 函数,其意为尝试释放锁。该函数返回一个 boolean 值,true 表示锁已释放,false 表示锁未释放。返回 false 是说明此时发生了锁重入,且在此时释放结束后重入次数并没有减少到 0。在未获得锁的情况下调用该函数时会抛出一场。其大致流程如下:
ReentrantLock#tryLock()
其内部实际上调用的是 ReentrantLock#Sync#tryAcquire() 函数的非公平实现,即其逻辑同 3.4.2.1.1 阻塞锁获取的 ReentrantLock#Sync#tryAcquire() 函数。
ReentrantLock#unlock()
其逻辑同 3.4.2.1.2 阻塞锁释放的 ReentrantLock#unlock() 函数。
ReentrantReadWriteLock 是 juc 包提供的读写锁。其中读锁是共享的,写锁是独占的。此锁适用于读多写少的场景,用读锁来控制读操作,写锁来控制写操作,较重量级锁而言会在一定程度上提高并发量。同时,读写、写读、写写是互斥的。读写锁的特点如下:
如上图所示为 ReentrantReadWriteLock 的类图。首先,其实现了 ReadWriteLock 接口,且实现了该接口定义的 readLock()、writeLock() 函数,其作用分别是获取读锁对象和获取写锁对象。其次,其定义了内部类 Sync 继承自 AQS,实现了 AQS 定义的 tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared()、isHeldExclusively() 函数,同时 Sync 又定义了 readerShouldBlock()、writerShouldBlock() 抽象函数。然后,又定义了 FairSync、NonFairSync 两个扩展自 Sync 的内部类,用来实现公平锁和非公平锁的功能,分别实现了 Sync 定义的 readerShouldBlock()、writerShouldBlock() 函数。最后,又定义了内部类 ReadLock、WriteLock 来作为读锁和写锁的实现,且二者都实现了 Lock 接口,同时,二者又聚合了 Sync 实例,以此来实现锁相关功能。
ReentrantReadWriteLock 在 state 的实现上,使用 state 的高 16 位来记录读锁,0 表示读锁未被持有,> 0 表示读锁被多个线程同时持有;使用 state 的低 16 位来记录写锁,0 表示写锁未被持有,> 0 表示写锁已被占有同时表示重入次数。
ReadLock#lock()
如上图所示,为读锁加锁的流程。读锁加锁使用 ReadLock 的 lock() 函数即可。根据读写锁的特点,在写锁未被持有、或写锁已被占有但写锁持有者为当前线程时可成功获取到读锁。其大致流程如下:
ReentrantReadWriteLock#Sync#tryAcquireShared()
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// ...
return 1;
}
return fullTryAcquireShared(current);
}
如上图所示,为 ReentrantReadWriteLock 的内部类 Sync 对 AQS 定义的 tryAcquireShared() 抽象函数的具体实现。具体功能为尝试一次读锁。其返回值共分为以下三种情况:
其大致流程如下:
AQS#doAcquireShared()
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
如图所示,为 AQS 的 doAcquireShared() 函数的具体实现。每个调用 tryAcquireShared() 函数获取读锁失败的线程都会进入该方法,在该方法内会再次调用 tryAcquireShared() 尝试获取一次,若失败则阻塞;否则则获取到锁且会持续唤醒队列中等待的线程(因为读锁是可共享的,若某个线程成功获取到读锁,则意味着队列中因读锁而等待的线程也有机会获取到读锁)。其大致流程如下:
ReadLock#unlock()
如上图所示,为读锁释放过程。读锁释放使用 ReadLock 的 unlock() 函数即可。该函数内部直接调用了 AQS 实现的 releaseShared() 函数,其大致流程如下:
ReentrantReadWriteLock#Sync#tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {
// ...
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
如上图所示,为内部类 Sync 实现的 AQS 定义的 tryReleaseShared() 抽象方法。该方法意为尝试释放共享锁,其返回一个 boolean 结果,true 表示读锁已完全释放;false 表示当前线程释放读锁,但其它持有读锁的线程还未释放。其实现原理是通过循环不断尝试修改 state 的的读锁状态值,若成功则返回。其大致流程如下:
AQS#doReleaseShared()
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
如上图所示,为 AQS 实现的 doReleaseShared() 的大概流程。该方法的作用是持续性唤醒阻塞队列中等待锁的线程。其逻辑是在死循环内,从第二个节点开始(第一个节点为头节点,头节点是哨兵节点,不关联线程),唤醒对应线程,让其去参与参与锁的竞争,直到队列中只有头节点时 break 循环返回。其大致流程如下:
WriteLock#lock()
如上图所示,为 WriteLock 写锁上锁流程。写锁上锁使用 WriteLock 的 lock() 方法即可。该方法内部直接调用了 AQS 实现的 acquire() 方法。其大致流程如下:
ReentrantReadWriteLock#Sync#tryAcquire()
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
如上图所示,为内部类 Sync 实现的 AQS 定义的 tryAcquire() 抽象方法的具体流程。其作用是尝试获取一次写锁。返回一个 boolean 值,true 表示获取成功,false 表示获取失败。其大致流程如下:
AQS#acquireQueued()
见 3.4.2.1.1 章节中对该方法的流程分析。
WriteLock#unlock()
如上图所示,为写锁解锁流程,写锁解锁使用 WriteLock 的 unlock() 方法即可。unlock() 内部直接调用了 AQS 实现的 release() 方法。其大致流程如下:
ReentrantReadWriteLock#Sync#tryRelease()
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
如上图所示,为内部类 Sync 实现的 AQS 定义的 tryRelease() 抽象方法的具体流程。其作用是尝试释放写锁,返回一个 boolean 值,true 表示写锁完全释放,false 表示只释放一次,还未完全释放(重入情况)。其大致流程如下:
StampedLock 是 jdk 8 引入读写锁,其目的是为了进一步提升 ReentrantReadWriteLock 读写锁中读的性能。当然实际上这两者之间在实现上并没有联系,只是有着相同的功能,同时前者在读锁的实现上比后者有着更加出色的性能。二者间的主要区别是:
StampedLock 在设计原理上类似于 AQS。首先,内部维护了一个被 volatile 修饰的长整型变量 state 用来表示锁状态;其次,使用一个 CLH 双向链表来承担阻塞队列的作用;最后,在读写锁的实现上,定义了两个内部类 ReadLockView、WriteLockView 来分别作为读写锁的实现,且其都实现了 Lock 接口。当然最重要的武器还是 CAS。
StampedLock 虽然提升了读的性能,但也有不足之处:
public class StampedLockTest {
// 创建锁对象
private final StampedLock lock = new StampedLock();
// 读数据
// 先用乐观读 若读取到的数据失效(读期间数据被其它线程修改)则乐观读升级为读锁
public Object read() {
// 获取乐观读的 戳
long stamp = lock.tryOptimisticRead();
// todo 读取数据
// 验戳
if (lock.validate(stamp)) {
// 若验证通过则说明读取到的数据未被其它线程修改 返回数据
return data;
}
// 若验戳失败 则说明读取期间数据已被其它线程修改 则乐观读升级为读锁 并重新读取数据
stamp = lock.readLock();
try {
// todo 重新读取数据
return data;
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}
// 写数据
public void write(Object data) {
long stamp = lock.writeLock();
try {
// todo 写数据
} finally {
lock.unlockWrite(stamp);
}
}
}
juc 包中除了提供锁相关实现外,还提供了大量的并发编程工具,包括线程池、原子类、同步器、并发容器。
juc 包中提供了关于线程池操作的实现,如 ThreadPoolExecutor、ScheduledThreadPoolExecutor 等。可以用来自定义创建适合多种业务场景的线程池。
在需要多线程处理工作任务的业务场景中,频繁的创建和销毁线程是非常消耗资源的,所以就有了线程池。顾名思义,线程池是存放线程的池子,池子里的线程可以不停歇的处理任务,当无任务可处理时线程可销毁可存活。这样就避免了频繁创建和销毁线程带来的资源消耗问题。
如上图所示,线程池是经典的 生产者-消费者 设计模式。用一个阻塞队列(Blocking Queue)来存放要处理的任务。主线程或其它线程(生产者)负责产生要处理的任务并放入阻塞队列。线程池(Thread Pool)中维护了一定的工作线程,负责消费阻塞队列中的任务。
线程池的具体实现中(ThreadPoolExecutor)用一个 AtomicInteger 变量来存储线程池状态等信息。其中高 3 位来表示线程池的状态,低 29 位来表示线程池中工作线程的数量。这样就可以在一次 CAS 操作中同时更新线程池状态和工作线程数量信息。
如上图所示,为 juc 提供的线程池相关类的类关系图。
Executor: 顶层接口,主要定义了 execute() 方法。其作用是向线程池中提交任务或让线程池执行任务。
void execute(Runnable command);
ExecutorService: 继承自 Executor 接口。增加了操作线程池的方法定义,如 shutdown()、shutdownNow()、isShutdown() 等。同时还增加了更多提交任务或线程池执行任务的方法定义,如 submit()、invokeAll()、invokeAny() 等。方法详细作用将在下节进行描述。
ScheduledExecutorService: 继承自 ExecutorService 接口。为任务调度线程池接口,增加了任务调度相关的方法,如 schedule()、scheduleAtFixedRate()、scheduleWithFixedDelay() 等。
AbstractExecutorService: Executor 接口的抽象实现,其实现了线程池接口中定义的大部分共用功能。
ThreadPoolExecutor: 线程池的具体实现,维护了线程池状态、工作线程、任务队列等,提供了适用于不同业务场景的线程池的构造方法,实现了线程池工作的具体流程。
ScheduledThreadPoolExecutor: 任务调度线程池的具体实现,扩展自 ThreadPoolExecutor 实现,在其基础上实现了任务调用线程池的相关功能。
// 工作线程集合 创建的工作线程会被维护在该集合
// Worker 为内部类 对 Thread 类进行了扩展
private final Set<Worker> workers = new HashSet<>();
// 任务队列/阻塞队列 提交到线程池的任务会被缓存至该队列
private final BlockingQueue<Runnable> workQueue;
// 核心线程数
private volatile int corePoolSize;
// 最大线程数
private volatile int maximumPoolSize;
// 救急线程存活时间(corePoolSize - maximumPoolSize = 救急线程池数)
private volatile long keepAliveTime;
// 拒绝策略
// 拒绝策略是指任务队列已满后提交到线程池的任务的处理方式
private volatile RejectedExecutionHandler handler;
如上图所示,为 ThreadPoolExecutor 线程池工作流程。其流程分析大致如下:
简言之,当工作线程数还未达到核心线程数时,对于新来的任务将创建新的线程执行;当工作线程数达到核心线程数时,新来的任务将进入任务队列;当任务队列中任务数达到队列容量时,对于新来的任务将采取拒绝策略;工作线程会持续性执行队列中的任务,即某个线程执行完当前任务后会从队列中获取任务继续执行;当任务队列中无任务时工作线程会继续等待任务,若等待时间超过线程存活时间(keepAliveTime),此时救急线程将结束,核心线程将继续等待。
RejectedExecutionHandler 是 juc 提供拒绝策略接口,其采用了策略模式,在提供了几种策略之外还支持自定义策略,可在线程池构造时指定策略。juc 提供的策略实现以及其它常用的拒绝策略有以下几种:
ThreadPoolExecutor 提供多种不同类型线程池的构造器,而构造器的参数值直接决定了线程池的类型或工作原理。构造器全部入参如下:
ThreadPoolExecutor 线程池提供的构造器如下:
newFixedThreadPool: 固定线程池。
特点:
适用:任务量已知,相对耗时的任务。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool: 缓存线程池。
特点:
适用:任务密集,耗时较短的任务。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newSingleThreadPool: 单例线程池。
特点:
适用:任务执行存在先后顺序。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newScheduledThreadPool: 任务调度线程池。
特点:
使用:任务需延迟或反复执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
线程池常用方法如下:
/** 任务 执行、提交 相关 **/
// 执行任务或提交任务
void execute(Runnable comman);
// 提交任务 带返回值
<T> Future<T> submit(Callable<T> task);
// 批量提交任务 带返回值 待提交的任务都执行完毕后才会返回
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
// 批量提交任务 带返回值 带超时 若超时则只返回已执行完毕的任务结果集
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
// 批量提交任务 返回最先执行完的任务结果(此时其它任务取消)
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
// 批量提交任务 带超时 返回最先执行完的任务结果(此时其它任务取消)
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException, TimeoutException;
/** 线程池关闭相关 **/
// 关闭线程池 状态变为 SHUTDOWN 调用后将不再接受新任务,但正在执行的和队列中的任务都将正常执行完毕
void shutdown();
// 关闭线程池 状态变为 STOP 调用后不再接受新任务、打断正在执行的任务、抛弃队列中的任务 并返回队列中剩余任务集
List<Runnable> shutdownNow();
/** 其它辅助方法 **/
// 判断线程池是否处于 SHUTDOWN 状态 实际上只要线程池不处于 RUNNING 状态则都返回 true
boolean isShutdown();
// 主线程在指定时间内等待线程池工作结束 若指定时间内所有任务都执行完毕则返回 true 若超过指定时间则返回 false
// 主线程可根据等待结果做一些额外操作
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
在任务调度线程池加入 jdk 以前,可以使用 java.util.Timer 类来实现定时任务。其优点是简单易用;缺点是所有任务都由一个线程来执行,即任务串行执行,前一个任务执行延迟或异常都将影响后一个任务的执行。
任务调度线程池的优点是任务由固定数量的线程执行;任务可串行、可并行执行;任务执行延迟不会影响后续任务执行;任务异常则会创建新的线程补上;可灵活指定延迟时间、间隔时间。
任务调度线程池构造器:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
任务调度常用方法:
// 一次性延迟任务 及启动后 delay 时间后执行一次
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
// 一次性延迟任务 带返回值
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
// 延迟定时任务
// command: 任务
// initinalDelay: 初始延迟 即启动后将在 initinalDelay 时间后开始执行定时任务
// period: 间隔时间(会受到任务执行时间的影响 如 period 为 1s, 任务执行需 2s,则实际两次任务执行间隔为 0s)
// unit: initinalDelay 和 period 的单位
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
// 延迟定时任务
// 与 scheduleAtFixedRate 的区别是 delay 不受任务执行所需时间的影响
// delay 从上一次执行结束算起 到下一次执行开始
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
线程池中若不处理异常则会消耗性能。即当池中某个线程执行任务时出现异常,若不处理则线程中断,可能需要创建一个新的线程补上;若处理了异常则当前线程会去执行下一个任务。
Fork/Join 是 jdk 1.7 加入的一种线程池实现,它采用一种分治思想,适用于可进行任务拆分的业务场景。
任务拆分是指将一个大任务拆分为多个计算规则相同的小任务,直至不能拆分,然后对每个小任务进行计算,最后对结果进行合并。分治思想常用于跟递归相关的一些算法上,如归并排序、斐波那契数列等。
ForkJoinPool 会将多个小任务的执行交给多个线程去执行,但是任务的拆分需要自己实现。且其默认会创建和 cpu 核心数相同的线程数。
ForkJoinPool 为 fork/join 线程池实现,提交给该线程池的任务对象需要实现 RecursiveTask< T> 或 RecursiveAction 类,其中前者有返回值(范型 T 代表返回值类型),后者没有返回值;同时得实现其 compute() 抽象方法,作为任务执行内容。
下述例子对 m ~ n 之间的整数求和,即可用 fork/join 解决。
// 任务类
public class SumTask extends RecursiveTask<Integer> {
private int start;
private int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
// 以下两个判断可理解为递归结束条件
if (start == end) {
return start;
}
if (end - start == 1) {
return start + end;
}
// 分治
int mid = (start + end) / 2;
SumTask left = new SumTask(start, mid);
SumTask right = new SumTask(mid + 1, end);
left.fork();
right.fork();
// 结果求和
return left.join() + right.join();
}
}
// test
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new SumTask(1, 10));
juc 包中提供的具有原子性操作的类,如 AtomicInteger、AtomicLong、AtomicRefrence 等。可以在无锁的情况下进行线程安全的原子性操作。
原子类的实现原理是 CAS 和 volatile。首先 CAS 只是一种算法,即比较和替换,如果相等则替换,否则就失败;其次,并发情况下必然是多个线程同时访问共享变量,又因为 CPU 多级缓存的存在,导致共享变量会出现可见性问题(共享变量可见性问题本质上是多核 CPU 的私有缓存造成的),于是就需要保证共享变量的可见性,这时候 volatile 就派上用场了;最后,再结合 while (true) 之类的死循环就实现了无锁。所以从本质上来说,CAS、volatile、while (true) 是无锁的三大件。换言之,CAS 操作必须借助 volatile 来获取到变量最新值以达到比较替换的效果。
同时,juc 中所有 CAS 算法的实现,都是底层类 Unsafe 提供的。常用原子类如下:
juc 提供的原子整数类有 AtomicBoolean、AtomicInteger、AtomicLong。以 AtomicInteger 为例:
// 创建 AtomicInteger 对象 初始值赋 0
AtomicInteger atomicInteger = new AtomicInteger(0);
// 获取
System.out.println(atomicInteger.get()); // 0
// 先获取并自增 1 相当于 i++
System.out.println(atomicInteger.getAndIncrement()); // 0
// 先自增 1 并获取 相当于 ++i
System.out.println(atomicInteger.incrementAndGet()); // 2
// 先获取并自减 1 相当于 i--
System.out.println(atomicInteger.getAndDecrement()); // 2
// 先自减 1 并获取 相当于 --i
System.out.println(atomicInteger.decrementAndGet()); // 0
// 先获取并自增指定数
System.out.println(atomicInteger.getAndAdd(2)); // 0
// 先自增指定数并获取
System.out.println(atomicInteger.addAndGet(2)); // 4
// 先获取并修改
System.out.println(atomicInteger.getAndUpdate(i -> i - 2)); // 4
// 先修改并获取
System.out.println(atomicInteger.updateAndGet(i -> i - 2)); // 0
// 先获取并计算
// 其中第一个参数(2)为 第二个参数(lambda)中的 x, 第二个参数(lambda)中的 p 为当前值
System.out.println(atomicInteger.getAndAccumulate(2, (p, x) -> p + x)); // 0
// 先计算并获取
System.out.println(atomicInteger.accumulateAndGet(2, (p, x) -> p + x)); // 4
// cas
System.out.println(atomicInteger.compareAndSet(0, 1)); // false
juc 提供的原子引用类有 AtomicReference、AtomicStampedReference、AtomicMarkableReference。
AtomicReference
// 创建 AtomicReference 对象 并赋初值 "a" 初值默认为 null
AtomicReference<String> atomicReference = new AtomicReference<>("A");
// 获取
System.out.println(atomicReference.get()); // A
// 设置
atomicReference.set("B");
System.out.println(atomicReference.get()); // B
AtomicStampedReference
// 创建原子对象 第一个参数为引用对象 第二个参数为版本号
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("A", 0);
// 获取引用值
System.out.println(atomicStampedReference.getReference()); // A
// 获取版本号
System.out.println(atomicStampedReference.getStamp()); // 0
// cas 替换
// 参数一:期望引用值;参数二:修改引用值;参数三:期望版本号;参数四:修改版本号(期望版本号 + 1)
atomicStampedReference.compareAndSet("A", "B", 0, 0 + 1)
AtomicMarkableReference
// 创建原子对象 第一个参数为引用值 第二个参数为初始标记值
AtomicMarkableReference<String> atomicMarkableReference = new AtomicMarkableReference<>("A", false);
// 获取引用值
System.out.println(atomicMarkableReference.getReference()); // A
// cas 替换
// 参数一:期望引用值;参数二:修改引用值;参数三:期望标记值;参数四:修改标记值
atomicMarkableReference.compareAndSet("A", "B", false, true)
juc 提供的原子数组有 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。以 AtomicIntegerArray 为例:
// 创建原子对象 构造参数为数组
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[10]);
// 给指定索引设置值
atomicIntegerArray.set(0, 0);
// 获取指定索引元素
System.out.println(atomicIntegerArray.get(0)); // 0
// cas 替换
// 参数一:要替换的元素索引值;参数二:指定元素的预期值;参数三:指定元素的修改值
atomicIntegerArray.compareAndSet(0, 0, 1);
juc 提供的原子字段更新器有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。以 AtomicIntegerFieldUpdater 为例:
// 测试实体类
public class TestVo {
private volatile int age; // 注:原子字段更新器操作的字段必须用 volatile 修饰
private volatile String name;
}
// 测试对象
TestVo testVo = new TestVo();
// 创建原子字段更新器 构造器第一个参数为要更新字段所在类的类类型 第二个参数为要更新字段名
AtomicIntegerFieldUpdater<TestVo> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(TestVo.class, "age");
// 获取字段值
System.out.println(fieldUpdater.get(testVo));
// 修改字段值
fieldUpdater.set(testVo, 1);
// cas 替换
// 参数一:对象;参数二:字段预期值;参数三:字段更新值
fieldUpdater.compareAndSet(testVo, 1, 2);
原子累加器,即 LongAdder,它亦是大哥李的杰作(这人实在太牛逼了!)。它的作用同 AtomicLong,但比 AtomicLong 要更快!更快!更快!。其核心属性如下:
// 累加单元数组 懒惰初始化
transient volatile Cell[] cells;
// 当没有竞争时 在该变量上累加 有竞争时在累加单元上累加 最终结果是 base + cells 数组里的值
transient volatile long base;
// cells 锁状态 0: 未被占有; 1: 被占有 当初始化 cells 或扩容 或创建某个具体的元素时需加锁 防止重复操作
transient volatile int cellsBusy;
LongAdder 在设计上,采用了有点类似于分治的思想。其内部维护了一个 Cell[] 数组,且数组大小最大等于 CPU 核数,即每个 CPU 都会对应一个 cell 元素,多线程情况下按照 CPU 分组在其对应的 cell 对象里进行累加,获取结果再将数组里的值进行求和即可。简言之,增加了多个累加单元,减少了线程 cas 失败时的重试次数,从而提高性能。
其 Cell 类上添加了 @jdk.internal.vm.annotation.Contended 注解,这和 伪共享 有关,而伪共享则和 CPU 高速缓存的缓存行有关。
CPU 缓存行是以缓存行为最小单位进行缓存数据的,一个缓存行对应内存中一块数据(注意是一块,不是一个),缓存行大小一般为 64 byte,也就是说,CPU 每次会从内存中获取总大小小于 64 byte 的一堆数据放入一个缓存行。缓存行失效的条件时只要缓存行那某个数据被修改,那这个缓存行就判定为失效。
cells 是个数组,数组中的元素在内存中是连续存放的。一个 Cell 元素占 24 byte(数组对象对象头占 16 byte,value 占 8 byte),CPU 在缓存 cells 数组时可能会将两个相邻的 cell 元素缓存在同一个缓存行里( (24 + 24) < 64)。这种情况下,就会有以下结果:
那么问题就来了,thread-1 更新 cell[0] 时会导致所有 CPU 里缓存了 cell[0] 和 cell[1] 的缓存行都失效;同理 thread-2 更新 cell[1] 时也会让所有 CPU 里缓存了 cell[0] 和 cell[1] 的缓存行都失效。简言之,对于当前正在执行 thread-1 代码的 CPU 来说,虽然我缓存了 cell[0],但它失效了,thread-2 同理。
对于 伪共享 的解决方法是:每次缓存时我只从内存中加载我需要的那部分数据,然后放到缓存行,即缓存行里只有 cell[0] 或 cell[1]。而 @jdk.internal.vm.annotation.Contended 注解,就是解决这个问题的。其原理是在被此注解作用的类的对象或属性前后各增加 128 byte 大小的 padding(参考前端 css 代码中的 padding),换言之,莫挨老子。这样就防止多个数据被加载至统一缓存行。从而解决伪共享问题。
Unsafe 即 sun.misc.Unsafe 类,是 jdk 提供的最底层的操作内存、线程的类。名为不安全的,实际是指该类不适合编程人员直接操作,因为是直接操作内存等,会造成不可预估的错误。同时该类对象不能直接创建,只能通过反射创建。以下以自定义实现 AtomicInteger 原子类为例,展示 Unsafe 用法。
public class CustomAtomicInteger {
// Unsafe 实例
private static final Unsafe unsafe;
// value 属性在其所在类中的偏移量
private static final long offset;
// value 用来存放 int 值(cas 操作的共享变量必须配合 volatile 使用)
private volatile int value;
static {
try {
// 通过反射获取 Unsafe 类中 theUnsafe 属性(Unsafe 是个单例类 持有了自身实例 但只能反射获取)
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 修改其访问权限
field.setAccessible(true);
// 获取属性值
unsafe = (Unsafe) field.get(null);
// 获取指定类中指定属性的偏移量
offset = unsafe.objectFieldOffset(CustomAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RunTimeException(e);
}
}
public CustomAtomicInteger(int value) {
this.value = value;
}
public boolean compareAndSet(int expect, int update) {
while (true) {
if (unsafe.compareAndSwapInt(this, offset, expect, update)) {
return true;
}
}
}
public int get() {
return value;
}
public void set(int newValue) {
this.value = value;
}
}
juc 包中提供一些具有线程同步功能的类,如 Semaphore、CountDownLatch、CyclicBarrier 等。可以协调多线程间的执行顺序和访问控制等。
Semaphore 即信号量,它可以用来限制并发访问同步资源的线程数。可以应用于连接池最大连接数等场景。使用实例如下:
@Slf4j
public class SemaphoreTest {
public static void main(String[] args) {
// 构造参数表示并发线程数 即同时只能有最多三个线程访问并发资源
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("start...");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end...");
semaphore.release();
}).start();
}
}
}
21:57:47.274 [Thread-2] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:47.274 [Thread-0] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:47.274 [Thread-1] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:49.280 [Thread-1] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
21:57:49.280 [Thread-2] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
21:57:49.280 [Thread-0] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
21:57:49.281 [Thread-5] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:49.281 [Thread-3] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:49.281 [Thread-6] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:51.284 [Thread-5] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
21:57:51.286 [Thread-4] INFO org.xgllhz.juc.sync.SemaphoreTest - start...
21:57:51.287 [Thread-6] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
21:57:51.289 [Thread-3] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
21:57:53.289 [Thread-4] INFO org.xgllhz.juc.sync.SemaphoreTest - end...
Semaphore 在实现上依然依赖于 AQS。其构造入参可理解为共享锁的最大上限(实则为锁状态 state 的初值),调用 acquire() 方法可理解为获取共享锁,只有当共享锁持有数量小于共享锁上限时才能获取到,获取成功后 state++;调用 release() 方法可理解为释放共享锁,释放成功后 state–,并唤醒阻塞线程。
Semaphore 在实现上亦有公平和非公平两种。公平意味着当共享资源占有线程数达到上限后,后来者会直接进入阻塞队列排队等待;非公平意味着当共享资源占用线程数达到上限后,后来者会先尝试竞争一次,若竞争失败才会进入阻塞队列中排队等待。且其默认为非公平实现。
semaphore.acquire() 核心源码如下:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 调用 tryAcquireShared 方法尝试获取一次
// 其返回值 < 0 则表示获取失败;> 0 则表示获取成功,同时表示剩余上限
if (tryAcquireShared(arg) < 0)
// 若失败后则进入阻塞队列等待
doAcquireSharedInterruptibly(arg);
}
// 非公平实现 由 Semaphore 内部类 Sync 的子类 NonfairSync 实现
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// AQS 中实现 这种套路在 ReentrantLock 和 ReentrantReadWriteLock 中已分析过多次 可参考
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
CountDownLatch 即计数器,可用来实现线程间同步写作,如等待其它线程执行结束。其构造参数接收一个整数值 n,表示主线程将等待 n 个线程执行完成后才继续运行。这 n 个线程中的每个线程在运行结束前需要将计数器值减一。可应用于多线程执行结果汇总、多线程远程调用(微服务调用)等场景。其使用示例如下:
@Slf4j
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
// 构造入参为 3 表示主线程需要等待三个线程执行结束
CountDownLatch latch = new CountDownLatch(3);
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.execute(() -> {
log.info("start...");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end...");
latch.countDown(); //线程内容运行完后计数器减一
});
threadPool.execute(() -> {
log.info("start...");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end...");
latch.countDown(); //线程内容运行完后计数器减一
});
threadPool.execute(() -> {
log.info("start...");
try {
Thread.sleep(1500L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end...");
latch.countDown(); //线程内容运行完后计数器减一
});
log.info("start...");
latch.await(); // 主线程等待其它三个线程执行完成
log.info("end...");
threadPool.shutdown();
}
}
CountDownLatch 的实现依然依赖于 AQS,核心方法为 countDown() 和 await(),为 AQS 家族中最简单的一个,请自行阅读源码。
CyclicBarrier 即循环栅栏,可用来进行线程协作,当计数器达到指定值后才开始运行。其构造器入参接收两个参数,第一个表示计数器预值,第二个参数表示当计数器达到预值后要执行的操作。子线程执行完后会将计数器加一,直到计数器达到预值后才开始执行第二个参数对应的操作。其使用示例如下:
@Slf4j
public class CyclicBarrierTest {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
// 2 表示当计数器达到 2 时开始执行第二个参数对应的内容
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
log.info("thread1 thread2 started...");
});
new Thread(() -> {
log.info("start...");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end...");
try {
barrier.await(); // 线程内容执行完后将计数器加一
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(() -> {
log.info("start...");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("end...");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}
}
CyclicBarrier 在实现上依赖于 ReentrantLock。其与 CountDownLatch 的区别是:
juc 包中提供的一些线程安全的容器,如 ConcurrentHashMap、BlockingQueue、CopyOnWriteArrayList 等。可以在多线程下安全使用。
如上图所示,为 jdk 提供的主要的线程安全集合类实现。共分为三大类,分别是旧版本遗留的实现、Collections 类中通过装饰器修饰的实现、juc 包中提供的实现。
旧版本遗留的:
这类安全容器是 jdk 旧版本中的实现,如 Vector、HashTable。其实现原理是 synchronized 关键字修饰相关方法。
Collections 装饰的:
这类安全容器由 Collections 类通过诸如 synchronizedLsit() 此类方法提供,其实现涉及到 List、Set、Map。其实现原理是装饰器模式。具体为定义内部类(如 SynchronizedLsit< E>),通过 synchronized 关键修饰对外提供的方法。
juc 包提供的:
这类安全容器由 juc 包提供,大致可分为 blocking 系列、concurrent 系列、copy on write 系列,如 ArrayBlockingQueue、ConcurrentHashMap、CopyOnWriteArrayList 等。
主要说明 LinkedBlockingQueue、ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList。
LinkedBlockingQueue 是线程安全的链表实现,其在线程安全方面的设计是使用两把锁(ReentrantLock)和 Dummy 节点。一把锁头节点,解决 take 时的安全问题;一把锁尾节点,解决 put 时的安全问题。Dummy 节点亦可理解为占位节点,take 锁永远锁住的是 Dummy 节点,Dummy 节点的存在保证了当链表中只有一个节点时 take 线程和 put 线程不会竞争(因为 take 锁锁住的是 dummy 节点,put 锁锁住的是唯一的元素节点)。两把锁的优点是保证了入队和出队没有竞争,在一定程度上提高了并发度。
LinkedBlockingQueue 与 ArrayBlockingQueue 的区别是:
ConcurrentHashMap 在 jdk 1.7 和 1.8 中的实现并不相同,其各自结构如上图所示。
结构:
并发设计
构造函数:
1.7:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {...}
注:除了 Segment 数组,其余数组皆为懒惰初始化。
1.8:
同 1.7。不同点在于 1.8 中的 concurrencyLevel 仅做判断使用,保证了 initialCapacity 最小值为 concurrencyLevel。
注:所有数组皆为懒惰初始化。
put:
get:
size:
ConcurrentLinkedQueue 在设计上与 LinkedBlockingQueue 相同,不同点在于锁的使用上,前者使用 cas 来实现,后者使用 ReentrantLock 来实现。Tomcat 中的 Acceptor(生产者)与 Poller(消费者)即采用了 ConcurrentLinkedQueue 队列实现。
CopyOnWriteArrayList 在设计上采用了写入时拷贝的思想,即在增删改操作时会将底层数组拷贝一份,在拷贝出来的数组上进行增删改操作。优点是读写分离,写入时不影响并发读。写入时拷贝数组较耗时,故其适合读多写少的场景。在 jdk 1.8 版本中使用 ReentrantLock 锁,而在 jdk 11 版本中优化为 synchronized。
缺点是在 get 时存在弱一致性。即当读取操作和写入操作同时发生,且二者操作的目标为同一个元素(如读取和更新同一个元素)时,读取操作获取到的结果可能时写入操作前的旧值,这便是弱一致性。根据 CAP 理论,弱一致性不可避免。数据库的 MVCC 机制即是弱一致性的表现。
并发编程模式是指在 java 并发编程中一些常用的编程设计套路,这些套路可以帮助我们设计出更合理、更优雅的程序。大致有以下几种:保护性暂停、犹豫、顺序控制、生产者/消费者、工作线程、两阶段终止、单例、享元等。
保护性暂停,即一个线程等待另一个线程的执行结果,或一个线程的执行结果需要传递到另一个线程中。其特点如下:
// 结果媒介
public class Response {
private final Object lock = new Object(); // 锁对象 使用其 wait/notify 来实现等待和唤醒
private Object result; // 执行结果
// 获取结果
public Object get() {
synchronized (lock) { // 获取锁
while (result == null) { // 当结果为 null 时阻塞等待 当被唤醒时继续判断
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return result; // 被唤醒后若结果不为 null 则返回结果
}
}
// 提交结果
public void complete(Object result) {
synchronized (lock) { // 获取锁
this.result = result;
lock.notifyAll(); // 唤醒等待在该锁对象上的所有线程
}
}
}
// 测试
Response response = new Response(); // 创建媒介对象
// 创建需要执行结果的线程并启动
new Thread(() -> {
Object result = response.get();
log.info("result = {}", result);
}).start();
// 创建产生执行结果的线程并启动 假设耗时 2s
new Thread(() -> {
log.info("start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
response.complete("zed");
log.info("end...");
}).start();
// 测试结果为:等待线程将在两秒后成功获取到结果
// 超时版本与普通版本唯一区别在于获取方式的实现不同
// 入参表示当等待线程等待时间超过 millis 毫秒后不管执行线程是否执行结束都会返回
// 因为执行线程唤醒方式为 notifyAll() 即唤醒所有等待在该锁对象上的线程 唤醒后这些线程将再次竞争锁
// 竞争失败的会再次阻塞等待 此时就需要重新计算上一次已等待的时间 即 当前时间 - 开始等待时间
public Object get(long millis) {
synchronized (lock) { // 获取锁
long start = System.currentTimeMillis(); // 开始等待时间
long waited = 0; // 已等待时间
while (result == null) {
long remain = millis - waited; // 剩余等待时间
if (remain <= 0) { // 等待超时时返回
break;
}
try {
lock.wait(remain);
} catch (InterrutedException e) {
e.printStackTrace();
}
waited = System.currentTimeMillis() - start; // 剩余等待时间
}
return result;
}
}
多任务版本可理解为多个线程同时等待各自对应线程的执行结果。以邮递员派送邮件给用户为例。
// 邮箱
public class Mailbox {
private Integer id; // 门牌号
private Object mail; // 邮件
public Mailbox(Integer id) {
this.id = id;
}
// 用户收取邮件(超时版本 即在指定时间未获取到邮件将放弃)
public Object getMail(long timeout) {
synchronized (this) {
long start = System.currentTimeMillis();
long waited = 0;
while (mail == null) {
long remain = timeout - waited;
if (remain <= 0) {
break;
}
try {
wait(remain);
} catch (InterruptedException e) {
e.printStackTrace();
}
waited = System.currentTimeMillis() - start;
}
// 收取完邮件后邮箱置空
Object result = mail;
mail = null;
return result;
}
}
// 邮递员派送邮件(即将邮件放入邮箱)
public void postMail(Object mail) {
synchronized (this) {
this.mail = mail;
notifyAll();
}
}
public Integer getId() {
return id;
}
}
// 小区邮局(即类似于楼下楼道内的邮箱架)
public class Postoffice {
// 整栋楼那所有住户的邮箱
private static final Map<Integer, MailBox> boxes = new HashTable<>();
private static Integer id = 1; // 初始门牌号
// 创建邮箱(即住户的邮箱由邮局管理)
public static Mailbox createMailbox() {
Mailbox box = new Mailbox(generateId());
boxes.put(box.getId(), box);
return box;
}
// 获取邮箱 即定位邮箱
public static Mailbox getMailbox(Integer id) {
return boxes.get(id);
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
private static Integer generateId() {
return id++;
}
}
// 用户 实现收取邮件的动作
public class People extends Thread {
@Override
public void run() {
Mailbox box = Postoffice.createMailbox();
log.info("{} start get mail...", box.getId());
Object mail = box.getMail(2000);
System.out.println(mail);
log.info("{} end get mail...", box.getId());
}
}
// 邮递员 实现派送邮件的动作
public class Postman extends Thread {
private Integer id; // 此次派送的目标住户(假设每次只送一户)
private Object mail; // 邮件
public Postman(Integer id, Object mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
Mailbox box = Postoffice.getMailbox(id);
log.info("start post mail to {}", id);
box.postMail(mail);
log.info("end post mail to {}", id);
}
}
// test 三户用户收取邮件 由三个邮递员派送
for (int i = 0; i< 3; i++) {
new People().start();
}
Thread.sleep(1000);
for (Integer id : Postoffice.getIds()) {
new Postman(id, "content " + id).start();
}
// console
10:25:39.475 [Thread-1] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.People - 3 start get mail...
10:25:39.475 [Thread-0] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.People - 2 start get mail...
10:25:39.475 [Thread-2] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.People - 1 start get mail...
10:25:40.503 [Thread-5] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.Postman - start post mail to 1
10:25:40.503 [Thread-3] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.Postman - start post mail to 3
content 1
content 3
10:25:40.503 [Thread-5] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.Postman - end post mail to 1
10:25:40.504 [Thread-1] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.People - 3 end get mail
10:25:40.503 [Thread-4] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.Postman - start post mail to 2
10:25:40.503 [Thread-3] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.Postman - end post mail to 3
content 2
10:25:40.503 [Thread-2] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.People - 1 end get mail
10:25:40.504 [Thread-0] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.People - 2 end get mail
10:25:40.504 [Thread-4] INFO org.xgllhz.juc.pattern.guarded_suspension.multi_task.Postman - end post mail to 2
犹豫模式,即若一个线程发现另一个线程获取当前线程已经完成了某件事,则本线程将无需再做。如系统监控任务/线程,系统监控任务只需启动一个实例即可,若已启动则后续线程将无需再启动。同时,还可用于线程安全的单例设计模式中。
// 当多次点击打开监控页面时 实际只会启动一个监控实例/线程
public class Monitor {
private volatile boolean isStart;
// 此处使用 DCL(双重检查锁)
public void start() {
if (isStart) {
return;
}
synchronized (this) {
if (isStart) {
return;
}
isStart = true;
// todo 启动监控线程
}
}
}
顺序控制,即某个操作先执行,某个操作后执行。如先输出 1 在输出二。还有交替执行(交替输出)。
private static final Object lock = new Object(); // wait/notify 的锁对象
private static boolean first = false; // 线程 1是否执行状态
public static void test() {
new Thread(() -> {
log.info("thread 2 start");
synchronized (lock) { // 线程二获得锁
// 若线程一未执行则等待 因为会存在虚假唤醒
// 即有多个等待在该锁对象上的线程 线程二被唤醒后未获取到锁 则需要重新等待阻塞 所以需要用 while (true) 死循环
while (!first) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("thread 2");
log.info("thread 2 end");
}
}).start();
new Thread(() -> {
log.info("thread 1 start");
synchronized (lock) { // 获取锁
log.info("thread 1");
first = true; // 修改状态
lock.notifyAll(); // 唤醒等待在该锁对象上的所有线程
}
log.info("thread 1 end");
}).start();
}
12:14:54.125 [Thread-0] INFO org.xgllhz.juc.pattern.order_control.OrderTest - thread 2 start
12:14:54.125 [Thread-1] INFO org.xgllhz.juc.pattern.order_control.OrderTest - thread 1 start
12:14:54.127 [Thread-1] INFO org.xgllhz.juc.pattern.order_control.OrderTest - thread 1
12:14:54.127 [Thread-1] INFO org.xgllhz.juc.pattern.order_control.OrderTest - thread 1 end
12:14:54.127 [Thread-0] INFO org.xgllhz.juc.pattern.order_control.OrderTest - thread 2
12:14:54.127 [Thread-0] INFO org.xgllhz.juc.pattern.order_control.OrderTest - thread 2 end
缺点:使用不便,代码繁琐;存在虚假唤醒,增加开销;wait-notify 有着严格的先后执行次序。
Thread t2 = new Thread(() -> {
log.info("thread 2 start");
LockSupport.park();
log.info("thread 2");
log.info("thread 2 end");
});
Thread t1 = new Thread(() -> {
log.info("thread 1 start");
log.info("thread 1");
LockSupport.unpark(t2);
log.info("thread 1 end");
});
t2.start();
t1.start();
unpark 可指定唤醒某个线程,且不需要锁对象的支持。
线程 1 输出 a,线程 2 输出 b,线程 3 输出 c,共循环 5 次(即交替五次),最终结果为 abcabcabcabcabc。
public class Node {
private int flag;
private int loopNumber;
public Node(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
// current: 当前标志 next: 下次标志 content: 当前输出内容
public void test(int current, int next, String content) {
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (flag != current) {
try {
this.wait();
} catch (InterrupedException e) {
e.printStackTrace();
}
}
System.out.print(content);
flag = next;
this.notifyAll();
}
}
}
}
Node node = new Node(1, 5);
new Thread(() -> {
node.test(1, 2, "a");
}).start();
new Thread(() -> {
node.test(2, 3, "b");
}).start();
new Thread(() -> {
node.test(3, 1, "c");
}).start();
public class LockNode extends ReentrantLock {
private int loopNumber;
public LockNode(int loopNumber) {
this.loopNumber = loopNumber;
}
// 开始方式 唤醒等待在第一个条件变量上的线程
public void start(Condition first) {
this.lock();
try {
System.out.println("start");
first.signal();
} finally {
this.unlock();
}
}
public void test(Condition current, Condition next, String content) {
for (int i = 0; i < loopNumber; i++) {
this.lock();
try {
current.await(); // 第一次执行到此处时先阻塞等待 等待被唤醒
System.out.print(content);
next.signal(); // 唤醒等待在下一个条件变量上的线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
}
LockNode lock = new LockNode(5);
Condition a = lock.newCondition();
Condition b = lock.newCondition();
Condition c = lock.newCondition();
new Thread(() -> {
lock.test(a, b, "a");
}).start();
new Thread(() -> {
lock.test(b, c, "b");
}).start();
new Thread(() -> {
lock.test(c, a, "c");
}).start();
public class Node {
private int loopNumber; // 循环次数
private Thread[] thread; // 线程数组
public Node(int loopNumber) {
this.loopNumber = loopNumber;
}
public void test(String content) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park(); // 当前线程第一次进入时先阻塞 等待被唤醒
System.out.print(content); // 被唤醒后打印内容
LockSupport.unpark(nextThread()); // 唤醒其后续线程
}
}
// 启动
public void start() { // 启动所有线程
for (Thread thread : threads) {
thread.start();
}
LockSupport.unpark(threads[0]); // 唤醒第一个线程
}
// 获取当前线程的下一个线程
public Thread nextThread() {
Thread currentThread = Thread.currentThread();
int index = 0; // 当前线程所在下标
for (int i = 0; i < threads.length; i++) {
if (currentThread == threads[i]) {
index = i;
break;
}
}
if (index < threads.length - 1) {
return threads[++index]; // 若未越界 则返回当前位置的下一个线程对象
} else {
return threads[0]; // 若越界 则说明本轮以打印完成(abc) 然后从头开始
}
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
}
Node node = new Node(5);
Thread t1 = new Thread(() -> { node.test("a") });
Thread t2 = new Thread(() -> { node.test("b") });
Thread t3 = new Thread(() -> { node.test("c") });
node.setThreads(t1, t2, t3);
node.start();
生产者/消费者模式,以队列为媒介,生产者只负责生产消息并入队,消费者事负责出队并消费,二者互不影响。其特点:
消息队列一般有单锁实现、双条件变量实现、双锁实现、分段锁实现等。
public MessageQueue<E> {
private Queue<E> queue;
private int capacity;
public MessageQueue(int capacity) {
this.queue = new LinkedList<>();
this.capacity = capacity;
}
public E take() {
synchronized (queue) {
while (queue.isEmpty()) { // 当队列为空时 阻塞消费者线程
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
E e = queue.poll(); // 取出一个元素后唤醒生产者线程(实际上阻塞在该队列上的所有线程都会被唤醒)
queue.notifyAll();
return e;
}
}
public void put(E e) {
synchronized (queue) {
while (queue.size() == capacity) { // 当队列已满时 阻塞生产者线程
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(e); // 添加一个元素后唤醒消费者线程(实际上阻塞在该队列上的所有线程都会被唤醒)
queue.notifyAll();
}
}
}
public class MessageQueue<E> {
private final ReentrantLock lock = new ReentrantLock();
// 读写条件变量
private final Condition notEmpty = lock.newCondition(); // 队列为空时作为阻塞或唤醒条件
private final Condition notFull = lock.newCondition(); // 队列满时作为阻塞或唤醒条件
private Queue<E> queue;
private int capacity;
public MessageQueue(int capacity) {
this.queue = new LinkedList<>();
this.capacity = capacity;
}
// 获取元素
public E take() {
lock.lock(); // 上锁
try {
while (queue.isEmpty()) { // 队列为空时 消费者线程等待在读锁条件上
try {
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
E e = queue.poll(); // 移除一个元素后 唤醒阻塞在写锁条件上的生产者线程
notFull.signal();
return e;
} finally {
lock.unlock(); // 解锁
}
}
// 生产元素
public void put(E e) {
lock.lock(); // 上锁
try {
while (queue.size() == capacity) { // 队列已满时 生产者线程阻塞等待在写锁条件上
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(e); // 添加一个元素后唤醒阻塞在读锁条件上的消费者线程
notEmpty.signal();
} finally {
putLock.unlock(); // 解锁
}
}
}
工作线程,即让有限的工作线程来轮流处理无限多的任务。线程池使其典型实现,同时特体现了享元设计模式。其优点是节省频繁创建和销毁线程带来的开销,便于管理线程等等。
线程池使用不当会带来饥饿现象,尤其在固定大小线程池中。饥饿现象是指当由多个环节组成的工作,任务以每个环节为单位提交到线程池中,线程池中的线程都被靠前环节的任务占用,以至于靠后环节的任务不能及时被执行。如在餐厅中,若把点餐、做菜、上菜看成一个流程的话,那么点餐、做菜、上菜则可以是这个流程中的三个环节,餐厅工作人员比作线程池中的工作线程。会出现饥饿现象的情况是每个工作人员负责每一批客人的点餐、做菜、上菜工作,当客人批次数大于工作人员后,后来的客人将会没人处理。
对于线程池饥饿现象,正确的做法是按照任务类型创建不同的线程池,在避免饥饿现象的同时还能提高处理效率。如线程池 a 专门负责点餐,线程池 b 专门负责做菜,线程池 c 专门负责上菜。
自定义线程池实现。主要包括自定义消息队列、工作线程、工作流程、拒绝策略等。
// 自定义拒绝策略接口 即当线程池中任务队列满了后 新添加的任务的处理方式 一般有以下几种
// 死等 queue.put(task);
// 超时等待 queue.put(task, 5, TimeUnit.SECONDS);
// 放弃任务 do nothing
// 抛出异常 throw new RuntimeException("队列已满");
// 提交线程自己执行 task.run();
@FunctionInterface
public interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
// 自定义阻塞队列 消息队列的双锁实现
public class BlockingQueue<E> {
// 读写锁
private final ReentrantLock takeLock = new ReentrantLock();
private final ReentrantLock putLock = new ReentrantLock();
// 读写锁的条件变量
private final Condition notEmpty = takeLock.newCondition();
private final Condition notFull = putLock.newCondition();
private final Queue<E> queue;
private final int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
this.queue = new LinkedList<>();
}
// 唤醒等待在读锁条件变量上的线程
private void signalNotEmpty() {
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
// 唤醒等待在写锁条件变上的线程
private void signalNotFull() {
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
// 阻塞获取元素
public E take() {
E element;
takeLock.lock();
try {
while (queue.isEmpty()) { // 队列为空时 消费者线程阻塞等待
log.info("queue is empty");
try {
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
element = queue.poll();
log.info("take");
} finally {
takeLock.unlock();
}
signalNotFull(); // 获取元素后唤醒等待在写锁条件变量上的生产者线程
return element;
}
// 超时阻塞获取元素
public E take(long timeout, TimeUnit timeUnit) {
E element;
takeLock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.isEmpty()) { // 队列为空 且 已阻塞时间仍在超时时间内时 消费者线程阻塞等待
log.info("queue is empty");
try {
if (nanos <= 0) {
return null;
}
nanos = notEmpty.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
element = queue.poll();
log.info("take");
} finally {
takeLock.unlock();
}
signalNotFull(); // 获取元素后唤醒等待在写锁条件变量上的生产者线程
return element;
}
// 阻塞添加元素
public void put(E element) {
putLock.lock();
try {
while (queue.size() == capacity) { // 当队列已满时 生产者线程阻塞等待
log.info("queue is full");
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(element);
log.info("put");
} finally {
putLock.unlock();
}
signalNotEmpty(); // 添加成功后唤醒等待则读锁条件变量上的消费者线程
}
// 超时阻塞添加元素
public void put(E element, long timeout, TimeUnit timeUnit) {
putLock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capacity) { // 队列已满 且 已阻塞时间仍在超时时间内时 生产者线程阻塞等待
log.info("queue is full");
try {
if (nanos <= 0) {
return;
}
nanos = notFull.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(element);
log.info("put");
} finally {
putLock.unlock();
}
signalNotEmpty(); // 添加成功后唤醒等待则读锁条件变量上的消费者线程
}
// 尝试阻塞添加元素
public void tryPut(RejectPolicy<E> rejectPolicy, E element) {
putLock.lock();
try {
if (queue.size() < capacity) { // 若队列未满 则添加元素
queue.add(element);
log.info("try put");
} else { // 若队列已满 则执行拒绝策略
log.info("execute reject-policy");
rejectPolicy.reject(this, element);
}
} finally {
putLock.unlock();
}
signalNotEmpty(); // 成功添加元素后 唤醒等待在读锁条件变量上的消费者线程
}
}
// 自定义线程池
public class ThreadPoolExecutor {
private final Set<Worker> workers = new HashSet<>(); // 工作线程集合
private final BlockingQueue<Runnable> taskQueue; // 任务队列
private final RejectPolicy<Runnable> rejectPolicy; // 拒绝策略
private final int coreSize; // 核心线程数
private final int capacity; // 任务队列容量
private final long timeout; // 获取或添加超时时间
private final TimeUnit timeUnit; // 超时时间单位
public ThreadPoolExecutor(RejectPolicy<Runnable> rejectPolicy, int coreSize, int capacity,
int timeout, TimeUnit timeUnit) {
this.rejectPolicy = rejectPolicy;
this.coreSize = coreSize;
this.capacity = capacity;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(capacity);
}
// 工作线程类
class Worker extends Thread {
private Runnable task; // 任务对象
public Worker(Runnable task) {
this.task = task;
}
// 工作线程工作原理
@Override
public void run() {
// 持续性执行任务队列中的任务 直至任务队列为空
while (task != null || (task = taskQueue.take(timeout, timeUnit)) != null) {
log.info("working...");
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) { // 任务队列为空时 溢出工作线程 即释放线程资源
log.info("remove worker");
workers.remove(this);
}
}
}
// 执行任务/提交任务
public void execute(Runnable task) {
synchronized (workers) {
if (workers.size() < coreSize) { // 当工作线程数小于核心线程数时 创建新的工作线程来执行任务
Worker worker = new Worker(task);
workers.add(worker);
worker.start();
} else { // 否则将任务添加到任务队列
taskQueue.tryPut(rejectPolicy, task);
}
}
}
}
// 创建线程池实例
ThreadPoolExecutor executor = new ThreadPoolExecutor(((taskQueue, task) -> {
task.run();
}), 3, 5, 2, TimeUnit.SECONDS);
for (int i = 0; i < 10; i++) {
int index = i;
executor.execute(() -> {
try {
Thread.sleep(1000L); // 假设每个任务耗时 1s
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("{}", index);
});
}
22:26:33.454 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - working...
22:26:33.454 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - try put
22:26:33.454 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - working...
22:26:33.456 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - try put
22:26:33.456 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - try put
22:26:33.456 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - try put
22:26:33.456 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - execute reject-policy
22:26:34.464 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 6
22:26:34.464 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 1
22:26:34.467 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - execute reject-policy
22:26:34.467 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - take
22:26:34.462 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 0
22:26:34.467 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - take
22:26:35.468 [main] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 7
22:26:35.470 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - working...
22:26:35.470 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - working...
22:26:36.473 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 2
22:26:36.474 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - take
22:26:36.474 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - working...
22:26:36.473 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 3
22:26:36.474 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - take
22:26:36.474 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - working...
22:26:37.477 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 4
22:26:37.478 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - queue is empty
22:26:37.478 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - 5
22:26:37.479 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - queue is empty
22:26:39.484 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - queue is empty
22:26:39.484 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - queue is empty
22:26:39.484 [Thread-1] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - remove worker
22:26:39.485 [Thread-0] INFO org.xgllhz.juc.pattern.work_thread.ThreadPool - remove worker
两阶段终止,即在一个线程内停止另一个线程。
关于停止线程,有两种不友好方式,即:
public class InterruptedStop {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()) { // 当线程被打断时 结束线程执行内容
log.info("料理后事");
break;
}
try {
Thread.sleep(1000L);
log.info("业务一");
} catch (InterruptedException e) { // 当线程在 sleep、wait 期间被打断时捕捉异常并设置打断标记
currentThread.interrupt();
}
log.info("业务二");
}
}, "monitor-thread");
thread.start();
}
public void stop() { // 利用打断方式停止线程
thread.interrupt();
}
}
InterruptedStop thread = new InterruptedStop();
thread.start();
Thread.sleep(3500L);
thread.stop();
23:17:44.672 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务一
23:17:44.676 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务二
23:17:45.676 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务一
23:17:45.677 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务二
23:17:46.680 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务一
23:17:46.680 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务二
23:17:47.169 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 业务二
23:17:47.170 [monitor-thread] INFO org.xgllhz.juc.pattern.two_phase_termination.InterruptedTest - 料理后事
public class InterruptedStop {
private Thread thread;
private volatile boolean isStop = false; // 利用 volatile 变量的可见性
public void start() {
thread = new Thread(() -> {
while (true) {
if (isStop) { // 为 true 时 结束线程执行内容
log.info("料理后事");
break;
}
try {
Thread.sleep(1000L);
log.info("业务一");
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("业务二");
}
}, "monitor-thread");
thread.start();
}
public void stop() {
isStop = true;
thread.interrupt(); // 打断线程 目的是让线程退出 sleep、wait 状态
}
}
即设计模式之单例模式,详见 [深入了解单例设计模式](人世间子 (xgllhz.top))。
即设计模式之享元模式,详见 [深入了解享元设计模式](人世间子 (xgllhz.top))。
享元模式的使用非常常见,如 jdk 中的包装类的缓存(如 Long 对 -128 ~ 127 之间的 Long 对象的缓存)、线程池、各种连接池等都属于享元模式的应用。
在使用享元模式设计连接池时需要考虑一下问题:
并发编程应用是指 java 并发编程经常涉及到的应用场景,或者说在这些场景下使用并发编程更加合理或可靠。并发编程应用场景大致有以下几种:效率、限制、互斥、同步和异步、缓存、分治、统筹、定时。
此效率可有两种理解,一是任务的处理效率,而是 CPU 的工作效率。
限制一般是指限制对资源的使用。如限制对 CPU 的使用、限制对共享资源的使用、单位时间内限流等等场景。
限制对 CPU 的使用是指当当前条件不满足线程执行时,通过 yield、sleep、wait 等方式使线程让出对 CPU 的使用权,以提高 CPU 的利用率。
sleep 实现
while (条件不满足) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// todo
wait 实现
synchronized (锁对象) {
while (条件不满足) {
try {
锁对象.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// todo
}
lock 条件变量实现
lock.lock();
try {
while (条件不满足) {
try {
条件变量.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// todo
} finally {
lock.unlock();
}
限制对共享资源的使用一般是指当某种共享资源是有限的时,可以通过并发编程的方式限制对其的使用。如使用信号量 Semaphore 限制对连接池的使用等等。有关 Semaphore 的介绍可见 4.3.1 章节。
public class ConnectionPool {
private final Connection[] connections; // 连接数组
private final AtomicIntegerArray states; // 连接状态 0: 空闲; 1: 繁忙
private final Semaphore semaphore; // 信号量 限制连接数
private final int poolSize; // 连接池大小
public ConnectionPool(int poolSize) {
this.connections = new Connection[poolSize]; // 初始化连接对象数组
this.states = new AtomicIntegerArray(new int[poolSize]); // 初始化连接状态
this.semaphore = new Semaphore(poolSize); // 初始化信号量 信号量大小和连接池大小保持一致
this.poolSize = poolSize;
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection(String.valueOf(i));
}
}
// 自定义实现连接对象
static class MockConnection implements Connection {
private String name;
public MockConnection(String name) {
this.name = name;
}
// ...
}
// 获取连接
public Connection getConnection() {
try {
semaphore.acquire(); // 若信号量不足 则阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) { // 若当前连接状态空闲 则尝试修改连接状态为繁忙
if (states.compareAndSet(i, 0, 1)) {
return connections[i];
}
}
}
}
// 释放连接
public void releaseConnection(Connection connection) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == connection) {
states.set(i, 0); // 修改该连接对应状态
semaphore.release(); // 释放信号量
break;
}
}
}
}
单位时间内限流是指在高并发场景下由于系统性能、程序性能或外部环境影响,使得程序在单位时间内只能接受一定数量的请求,以此来保证程序不瘫痪。可以使用 guava 实现的 RateLimiter 来实现。
@RestController
@RequestMapping("/test")
public class TestController {
private RateLimiter limiter = RateLimiter.create(1000);
@PostMapping("/test")
public String test() {
limiter.acquire();
return "ok";
}
}
互斥是指同一时刻只能有一个线程访问共享资源。可以通过悲观互斥和乐观重试两种方式实现。
悲观互斥
悲观互斥即悲观锁,具体实现是基于各种重量级锁,如 synchronized、lock 系列锁等。
乐观重试
乐观重试即乐观锁,具体实现是基于 while true + cas 算法。
while (true) {
if (cas()) {
break;
}
}
线程间的同步或异步可以基于线程执行结果来理解,若一个线程需要立刻得到另一个线程的执行结果则可称之为同步关系,反之则可称之为异步。线程间同步或异步可通过 join、Future、生产者/消费者模式、线程池等实现。
可以用同步实现,也可以用异步实现。
join(同步)
Object result;
Thread t1 = new Thread(() -> {
// 提交结果到 result
});
t1.start();
t1.join(); // 当前线程等待 t1 执行结束
其优点是使用方便。缺点是需要提供外部共享变量来承载结果,不符合面向对象封装的思想;不能配合线程池使用。
Future(同步)
FutureTask<Object> task = new FutureTask<>(() -> {
return "ok";
});
new Thread(task, "t1").start();
task.get(); // 获取结果
// 配合线程池使用
ExecutorService threadPool = Executors.newFixedThreadPool(1);
Future<Object> result = threadPool.submit(() -> {
return "ok";
});
result.get(); // 获取结果
其优点是规避了 join 的缺点(手动滑稽)。
自定义实现(同步)
即并发编程模式中的保护性暂停模式,即 5.1 章节。
CompletableFuture(异步)
ExecutorService computePool = Executors.newFixedThreadPool(1); // 计算线程池
ExecutorService resultPool = Executors.newFixedThreadPool(1); // 接受结果线程池
CompletableFuture.supplyAsync(() -> {
return "ok";
}, computePool).thenAcceptAsync((result) -> {
// result 为执行结果
}, resultPool);
其优点是:
生产者/消费者模式(异步)
即生产者负责生产结果,消费者负责处理结果,二者以阻塞队列为媒介,互不干扰。
此时最好用异步来实现。
普通线程
如普通 IO 操作。
线程池
线程池…说了好多了。往里塞就完事儿了。
并发编程与缓存实际上是指缓存和数据库一起使用(双写)时的数据一致性问题。即通过并发编程来解决 缓存 + 数据库 下的数据一致性问题。有关数据一致性问题,详见 [走进科学之《redis 的秘密》第十节](人世间子 (xgllhz.top))。
数据一致性问题是基于 CAP(Cache Aside Pattern)模式出现的,该模式是 缓存 + 数据库 双写背景下对数据读写的常用模式。具体为:
在高并发场景下,该模式就会出现数据一致性问题。一般会出现两种问题,即:
数据一致性问题,一般情况下可以通过操作串行化来处理,即将数据的读写请求入队,然后按入队顺序处理即可。但该方案对系统吞吐量影响较大,所以一般不建议使用。
// 模拟数据库操作
public class BaseMapper {
Object selectById(Serializable id) {
return new Object();
}
List<Object> selectList() {
return new ArrayList<>();
}
void updateById(Serializable id, Object data) {
return;
}
}
public class Cache {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); // 读锁
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); // 写锁
private final Map<String, Object> map; // 缓存集合
private final BaseMapper baseMapper;
public Cache() {
this.map = new HashMap<>();
this.baseMapper = new BaseMapper();
}
// 获取数据
public Object get(String key) {
Object result;
readLock.lock(); // 读取时加读锁 防止其它线程修改缓存
try {
if ((result = map.get(key)) != null) {
return result;
}
} finally {
readLock.unlock();
}
// 缓存为空 则差数据库并添加缓存
writeLock.lock(); // 加写锁 防止与其它线程操作并行 以影响结果
try {
if ((result = map.get(key)) != null) { // 在检查缓存 类似于 DCl
return result;
}
result = baseMapper.selectById(key);
map.put(key, result);
return result;
} finally {
writeLock.unlock();
}
}
// 更新数据
public void update(String key, Object data) {
writeLock.lock(); // 加写锁 防止其它线程读取或更改
try {
map.remove(key);
baseMapper.updateById(key, data);
} finally {
writeLock.unlock();
}
}
}
以上为缓存简易实现,只体现了读写锁在数据一致性问题中的应用,实际缓存实现中还应考虑一下问题:
分治即利用分治思想处理任务。适合用分治思想处理的任务一般都具有可大化小,小任务逻辑相同的特点。一般可通过多线程来处理小任务,然后使用线程安全的容器对执行结果进行统计来实现;第二种方式是使用 ForkJoinPool 线程池来解决,该线程池采用分治思想设计,适合处理可拆分的任务。
见 4.1.6.2 章节。
使用并发编程相关解决统筹学问题,一般可使用 join、wait/notify、LockSupport 及锁相关 api 来相互配合结合。
使用并发编程相关解决定时类问题,一般可使用任务调用线程池。任务调用线程池的优点是任务可串行执行、并行执行、延迟执行等等。
完结撒花!