
00:00:00
外卖卷:非常划算 扫码领劵 省点小钱钱
JUC 是 java.util.concurrent 包的缩写,是 Java 并发编程的核心工具包。它提供了一系列用于处理多线程编程的类和接口,旨在简化并发编程的复杂性,提高程序性能和可靠性。
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
需要等待结果返回,才能继续运行就是同步
不需要等待结果返回,就能继续运行就是异步
//创建线程对象
Thread t = new Thread() {
public void run() {
//要执行的任务
}
};
//启动线程
t.start();Thread 代表线程Runnable 可运行的任务(线程要执行的代码)Runnable runnable = new Runnable() {
public void run() {
//要执行的任务
}
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();Java8 以后可以使用 lambda 精简代码
//创建任务对象
Runnable task2 = () -> log.debug("hello");
//参数 1 是任务对象;参数 2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();Runnable 更容易与线程池等高级 API 配合Runnable 让任务类脱离了 Thread 继承体系,更灵活FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
//创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
//参数 1 是任务对象;参数 2 是线程名字,推荐
new Thread(task3, "t3").start();
//主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);tasklist 查看进程taskkill 杀死进程ps -fe 查看所有进程ps -fT -p <PID> 查看某个进程(PID)的所有线程kill 杀死进程top 按大写 H 切换是否显示线程top -H -p <PID> 查看某个进程(PID)的所有线程ps 命令查看所有 Java 进程jstack <PID> 查看某个 Java 进程(PID)的所有线程状态jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)需要以如下方式运行你的 java 类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java 类修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
如果要认证访问,还需要做如下步骤
jmxremote.password 文件jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写controlRole (用户名) , R&D (密码)JVM 中的栈(Java Virtual Machine Stacks)内存是为线程服务的。每个线程启动后,虚拟机会为其分配一块栈内存。
原因:由于以下一些原因导致 CPU 不再执行当前的线程,转而执行另一个线程的代码
sleep、yield、wait、join、park、synchronized、lock 等方法过程:当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。
状态:状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。
影响:上下文切换频繁发生会影响性能。
| 方法名 | 功能说明 | 注意 |
|---|---|---|
start() | 启动一个新线程,在新线程中运行 run() 方法中的代码 | start() 方法只是让线程进入就绪状态,里面的代码不一定立刻运行。每个线程对象的 start() 方法只能调用一次,否则会出现 IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run() 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 |
join() | 等待线程运行结束 | |
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | |
getId() | 获取线程长整型的 id | id 唯一 |
getName() | 获取线程名 | |
setName(String) | 修改线程名 | |
getPriority() | 获取线程优先级 | |
setPriority(int) | 修改线程优先级 | Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的几率 |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 |
isAlive() | 线程是否存活(还没有运行完毕) | |
interrupt() | 打断线程 | 如果被打断线程正在 sleep, wait, join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记 |
interrupted() **(static)*- | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() **(static)*- | 获取当前正在执行的线程 | |
sleep(long n) **(static)*- | 让当前执行的线程休眠 n 毫秒,休眠时会让出 cpu 的时间片给其它线程 | |
yield() **(static)*- | 提示线程调度器让出当前线程对 CPU 的使用 | 主要是为了测试和调试 |
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。
| 方法名 | 功能说明 |
|---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
默认情况下,Java 进程需要等待所有非守护线程都运行结束,才会结束。
有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
例:
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");注意
这是从操作系统层面来描述的。
这是从 *Java 语言- 层面来描述的。
start() 方法。start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)。wait()、join() 方法。sleep(long) 方法。t.start() 方法时,由 NEW -> RUNNABLEt 线程用 synchronized(obj) 获取了对象锁后
obj.wait() 方法时,t 线程从 RUNNABLE -> WAITINGobj.notify(),obj.notifyAll(),t.interrupt() 时 t 线程从 WAITING -> RUNNABLEt 线程从 WAITING -> BLOCKEDpublic class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}t.join() 方法时,当前线程从 RUNNABLE -> WAITINGt 线程对象的监视器上等待t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING -> RUNNABLELockSupport.park() 方法会让当前线程从 RUNNABLE -> WAITINGLockSupport.unpark(目标线程) 或调用了线程的 interrupt(),会让目标线程从 WAITING -> RUNNABLEt 线程用 synchronized(obj) 获取了对象锁后
obj.wait(long n) 方法时,t 线程从 RUNNABLE -> TIMED_WAITINGt 线程等待时间超过了 n 毫秒,或调用 obj.notify(),obj.notifyAll(),t.interrupt() 时 t 线程从 TIMED_WAITING -> RUNNABLEt 线程从 TIMED_WAITING -> BLOCKEDt.join(long n) 方法时,当前线程从 RUNNABLE -> TIMED_WAITINGt 线程对象的监视器上等待n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING -> RUNNABLEThread.sleep(long n),当前线程从 RUNNABLE -> TIMED_WAITINGn 毫秒,当前线程从 TIMED_WAITING -> RUNNABLELockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE -> TIMED_WAITINGLockSupport.unpark(目标线程) 或调用了线程的 interrupt(),或是等待超时,会让目标线程从 TIMED_WAITING -> RUNNABLEt 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE -> BLOCKEDobj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED -> RUNNABLE,其它失败的线程仍然 BLOCKED当前线程所有代码运行完毕,进入 TERMINATED
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
synchronized,Lock本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意
虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
synchronized 关键字用在方法上,锁住的是 this 对象。
class Test {
public synchronized void test() {
// ...
}
}
// 等价于
class Test {
public void test() {
synchronized(this) {
// ...
}
}
}synchronized 关键字用在静态方法上,锁住的是类对象。
class Test {
public synchronized static void test() {
// ...
}
}
// 等价于
class Test {
public static void test() {
synchronized(Test.class) {
// ...
}
}
}StringIntegerStringBufferRandomVectorHashtablejava.util.concurrent 包下的类这里说它们是线程安全的,是指多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:
以 32 位虚拟机为例
|----------------------------------------------------------------|
| Object Header (64 bits) |
| ---------------------------------------- | ----------------------- |
| Mark Word (32 bits) | Klass Word (32 bits) |
| ---------------------------------------- | ----------------------- ||----------------------------------------------------------------------------------|
| Object Header (96 bits) |
| -------------------------------- | ----------------------- | ------------------------- |
| Mark Word (32bits) | Klass Word (32bits) | array length (32bits) |
| -------------------------------- | ----------------------- | ------------------------- ||-------------------------------------------------------|---------------------|
| Mark Word (32 bits) | State |
| ------------------------------------------------------- | --------------------- |
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
| ------------------------------------------------------- | --------------------- |
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
| ------------------------------------------------------- | --------------------- |
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
| ------------------------------------------------------- | --------------------- |
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
| ------------------------------------------------------- | --------------------- |
| | 11 | Marked for GC |
| ------------------------------------------------------- | --------------------- ||----------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
| ---------------------------------------------------------------------- | -------------------- |
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
| ---------------------------------------------------------------------- | -------------------- |
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
| ---------------------------------------------------------------------- | -------------------- |
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
| ---------------------------------------------------------------------- | -------------------- |
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
| ---------------------------------------------------------------------- | -------------------- |
| | 11 | Marked for GC |
| ---------------------------------------------------------------------- | -------------------- |Monitor 被翻译为监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
Monitor 结构如下:
WaitSet:关联了所有处于 WAITING 状态的线程。EntryList:关联了所有处于 BLOCKED 状态的线程,它们都会竞争 Owner 的所有权。Owner:初始时为 null,当有线程占有该 Monitor 时,Owner 指向这个线程。注意:
synchronized。假设有两个方法同步块,利用同一个对象加锁:
static final Object obj = new Object();
public static void method1() {
synchronized(obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized(obj) {
// 同步块 B
}
}如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候待锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
| 线程 1 (cpu 1 上) | 对象 Mark | 线程 2 (cpu 2 上) |
|---|---|---|
| - | 10 (重量锁) | - |
| 访问同步块,获取 monitor | 10 (重量锁) 重量锁指针 | - |
| 成功 (加锁) | 10 (重量锁) 重量锁指针 | - |
| 执行同步块 | 10 (重量锁) 重量锁指针 | - |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 访问同步块,获取 monitor |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 自旋重试 |
| 执行完毕 | 10 (重量锁) 重量锁指针 | 自旋重试 |
| 成功 (解锁) | 01 (无锁) | 自旋重试 |
| - | 10 (重量锁) 重量锁指针 | 成功 (加锁) |
| - | 10 (重量锁) 重量锁指针 | 执行同步块 |
自旋重试失败的情况
| 线程 1 (cpu 1 上) | 对象 Mark | 线程 2 (cpu 2 上) |
|---|---|---|
| - | 10 (重量锁) | - |
| 访问同步块,获取 monitor | 10 (重量锁) 重量锁指针 | - |
| 成功 (加锁) | 10 (重量锁) 重量锁指针 | - |
| 执行同步块 | 10 (重量锁) 重量锁指针 | - |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 访问同步块,获取 monitor |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 自旋重试 |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 自旋重试 |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 自旋重试 |
| 执行同步块 | 10 (重量锁) 重量锁指针 | 阻塞 |
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
Java 7 之后不能控制是否开启自旋功能
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
例如:
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}偏向锁状态
一个对象创建时:
101,这时它的 thread、epoch、age 都为 0-XX:BiasedLockingStartupDelay=0 来禁用延迟-XX:-UseBiasedLocking 可以禁用偏向锁,-XX:+UseBiasedLocking 启用偏向锁,默认是启用撤销偏向锁的状态:
批量重偏向:
批量撤销:
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部
锁粗化(Lock Coarsening)是一种 JVM(Java 虚拟机)的优化技术,旨在减少不必要的锁获取和释放操作,从而提高程序的性能。
当 JVM 检测到一系列连续的操作都对同一个锁对象进行加锁和解锁时,它可能会将这些分散的加锁/解锁操作合并(粗化)为一个更大的锁区域,只在整个操作序列的开始处加锁,在结束处解锁。
一些看起来没有加锁的代码,其实隐式的加了很多锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以
API:
obj·wait() 让进入 object 监视器的线程到 waitSet 等待obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止wait(long n) 有时限的等待,到 n 毫秒后结束等待,或是被 notify它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
sleep(long n) 和 wait(long n):
区别:
共同点:
wait() 是 WAITING)虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程
解决方法:采用 notifyAll
notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断
解决方法:用 while + wait,当条件不成立,再次 wait
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
// 另一个线程
synchronized(lock) {
lock.notifyAll();
}即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);与 Object 的 wait/notify 相比
wait, notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必park/unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】park/unpark 可以先 unpark,而 wait/notify 不能先 notify(先 notify 是无效的)将锁的粒度细分
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
定位死锁:
活锁出现在两个线程互相改变对方的结束条件,最后准也无法结束
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
相对于 synchronized 它具备如下特点:
与 synchronized 一样,都支持可重入。
public void lockInterruptibly():获得可打断的锁
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断
public boolean tryLock():尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列
public boolean tryLock(long timeout, TimeUnit unit):在给定时间内获取锁,获取不到就退出
注意:tryLock 期间也可以被打断
构造方法:ReentrantLock lock = new ReentrantLock(true)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}ReentrantLock 默认是不公平的:
public ReentrantLock() {
sync = new NonfairSync();
}说明:公平锁一般没有必要,会降低并发度
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:
synchronized 是那些不满足条件的线程都在一间休息室等消息ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒使用流程:
await 前需要获得锁await 执行后,会释放锁,进入 conditionObject 等待await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁lock 锁成功后,从 await 后继续执行JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是不可变的,就算有缓存,也不会存在不可见的问题
main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
static boolean run = true; // 添加 volatile
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程 t 不会如预想的停下来
}原因:
解决方法
volatile(易变关键字):它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响
定义原子操作的使用规则:
有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序
CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种:
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令现代 CPU 支持多级指令流水线,几乎所有的冯•诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为五级指令流水线。CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率
处理器在进行重排序时,必须要考虑指令之间的数据依赖性
补充知识:
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
public class MonitorService {
// 用来表示是否已经有线程已经在执行启动了
private volatile boolean starting = false;
public void start() {
System.out.println("尝试启动监控线程...");
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
// 真正启动监控线程...
}
}对比保护性暂停模式:保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待
例子:希望 doInit() 方法仅被调用一次,下面的实现出现的问题:
public class TestVolatile {
volatile boolean initialized = false;
void init() {
if (initialized) {
return;
}
doInit();
initialized = true;
}
private void doInit() {
}
}volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性)
性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小
synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性
volatile 修饰的变量,可以禁用指令重排
指令重排实例:
example 1:
public void mySort() {
int x = 11; //语句 1
int y = 12; //语句 2 谁先执行效果一样
x = x + 5; //语句 3
y = x - x; //语句 4
}执行顺序是:1 2 3 4、2 1 3 4、1 3 2 4
指令重排也有限制不会出现:4321,语句 4 需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行
example 2:
int num = 0;
boolean ready = false;
// 线程 1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程 2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}情况一:线程 1 先执行,ready = false,结果为 r.r1 = 1
情况二:线程 2 先执行 num = 2,但还没执行 ready = true,线程 1 执行,结果为 r.r1 = 1
情况三:线程 2 先执行 ready = true,线程 1 执行,进入 if 分支结果为 r.r1 = 4
情况四:线程 2 执行 ready = true,切换到线程 1,进入 if 分支为 r.r1 = 0,再切回线程 2 执行 num = 2,发生指令重排
使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据
lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence)
内存屏障有三个作用:
保证可见性:
写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能
保证有序性:
不能解决指令交错:
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前
有序性的保证也只是保证了本线程内相关代码不被重排序
volatile i = 0;
new Thread(() -> {i++});
new Thread(() -> {i--});i++ 反编译后的指令:
0: iconst_1 // 当 int 取值 -1~5 时,JVM 采用 iconst 指令将常量压入栈中
1: istore_1 // 将操作数栈顶数据弹出,存入局部变量表的 slot 1
2: iinc 1, 1
happens-before 是 Java 内存模型(JMM)中定义的一系列规则,用于阐述操作之间的内存可见性。它解决了在多线程环境下,一个线程对共享变量的修改何时对另一个线程可见的问题。
简单来说,如果两个操作存在 happens-before 关系,那么前一个操作的执行结果对后一个操作是可见的。这是一种偏序关系。
把这个概念想象成一个“时间胶囊”。线程 A 把变量 x 的值从 0 改为 10,然后把这个结果(连同其他所有修改)一起打包放进一个“时间胶囊”里,并盖上了时间戳。如果线程 B 的某个操作被规定在线程 A 的这个操作“之后”发生(即满足 happens-before 规则),那么线程 B 就有权打开这个“时间胶囊”,并且能看到 x 的值已经是 10 了。
JMM 提供了以下几种天然的 happens-before 规则,你不需要 synchronized 也能保证可见性:
happens-before 于书写在后面的操作。这是最符合我们单线程编程思维的一条。unlock(解锁)操作 happens-before 于后面对同一个锁的 lock(加锁)操作。也就是说,如果一个线程释放了锁,那么它在临界区内做的所有修改,对下一个获得该锁的线程来说都是可见的。volatile 变量的写操作 happens-before 于后面对这个变量的读操作。这意味着一旦一个线程修改了 volatile 变量,新值对其他线程是立即可见的。start() 方法 happens-before 于此线程的每一个动作。happens-before 于对此线程的终止检测。我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行。interrupt() 方法的调用 happens-before 于被中断线程的代码检测到中断事件的发生。happens-before 于它的 finalize() 方法的开始。happens-before 操作 B,操作 B happens-before 操作 C,那么可以得出操作 A happens-before 操作 C。无锁编程:Lock Free
CAS 的全称是 Compare-And-Swap(比较并替换值)或者 Compare-And-Set(比较并设置值),是 CPU 并发原语
底层原理:CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核和多核 CPU 下都能够保证比较交换的原子性
程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果
程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行总线锁定或缓存锁定,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性
作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止
CAS 的特点:
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 缺点:
注意:
CAS 与 synchronized 总结:
常见原子类:AtomicInteger、AtomicBoolean、AtomicLong
构造方法:
public AtomicInteger():初始化一个默认值为 0 的原子型 Integerpublic AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer常用 API:
| 方法 | 作用 |
|---|---|
| public final int get() | 获取 AtomicInteger 的值 |
| public final int getAndIncrement() | 以原子方式将当前值加 1,返回的是自增前的值 |
| public final int incrementAndGet() | 以原子方式将当前值加 1,返回的是自增后的值 |
| public final int getAndSet(int value) | 以原子方式设置为 newValue 的值,返回旧值 |
| public final int addAndGet(int data) | 以原子方式将输入的数值与实例中的值相加并返回 实例:AtomicInteger 里的 value |
AtomicInteger 原理:自旋锁 + CAS 算法
CAS 算法:有 3 个操作数(内存值 V,旧的预期值 A,要修改的值 B)
分析 getAndSet 方法:
AtomicInteger:
public final int getAndSet(int newValue) {
/**
* this: 当前对象
* valueOffset: 内存偏移量,内存地址
*/
return unsafe.getAndSetInt(this, valueOffset, newValue);
}valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
//调用本地方法 -->
public native long objectFieldOffset(Field var1);unsafe 类:
// val1: AtomicInteger 对象本身,var2: 该对象值得引用地址,var4: 需要变动的数
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
// var5: 用 var1 和 var2 找到的内存中的真实值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据
变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量
private volatile int valueCAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果
分析 getAndUpdate 方法:
getAndUpdate:
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get(); //当前值,cas 的期望值
next = updateFunction.applyAsInt(prev);//期望值更新到该值
} while (!compareAndSet(prev, next));//自旋
return prev;
}函数式接口:可以自定义操作逻辑
AtomicInteger a = new AtomicInteger();
a.getAndUpdate(i -> i + 10);compareAndSet:
public final boolean compareAndSet(int expect, int update) {
/**
* this: 当前对象
* valueOffset: 内存偏移量,内存地址
* expect: 期望的值
* update: 更新的值
*/
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量
原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference
AtomicReference 类:
构造方法:AtomicReference<T> atomicReference = new AtomicReference<T>()
常用 API:
public final boolean compareAndSet(V expectedValue, V newValue):CAS 操作public final void set(V newValue):将值设置为 newValuepublic final V get():返回当前值public class AtomicReferenceDemo {
public static void main(String[] args) {
Student s1 = new Student(33, "z3");
// 创建原子引用包装类
AtomicReference<Student> atomicReference = new AtomicReference<>();
// 设置主内存共享变量为 s1
atomicReference.set(s1);
// 比较并交换,如果现在主物理内存的值为 z3,那么交换成 l4
while (true) {
Student s2 = new Student(44, "l4");
if (atomicReference.compareAndSet(s1, s2)) {
break;
}
}
System.out.println(atomicReference.get());
}
}
class Student {
private int id;
private String name;
// ...
}原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
AtomicIntegerArray 类方法:
/**
* i the index
* expect the expected value
* update the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field、字段、属性、成员变量)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常 IllegalArgumentException: Must be volatile type
常用 API:
static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> c, String fieldName):构造方法abstract boolean compareAndSet(T obj, int expect, int update):CASpublic class UpdateDemo {
private volatile int field;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater
.newUpdater(UpdateDemo.class, "field");
UpdateDemo updateDemo = new UpdateDemo();
fieldUpdater.compareAndSet(updateDemo, 0, 10);
System.out.println(updateDemo.field);//10
}
}原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
LongAdder 和 LongAccumulator 区别:
相同点:
不同点:
调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x)
LongAccumulator 类功能更加强大,构造方法参数中
public class TestFinal {
final int a = 20;
}字节码:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20 // 将值直接放入栈中
7: putfield #2 // Field a:I
<-- 写屏障
10: returnfinal 变量的赋值通过 putfield 指令来完成,在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况
其他线程访问 final 修饰的变量
不可变:如果一个对象不能够修改其内部状态(属性),那么就是不可变对象
不可变对象线程安全的,不存在并发修改和可见性问题,是另一种避免竞争的方式
String 类也是不可变的,该类和类中所有属性都是 final 的
类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
无写入方法(set)确保外部不能对内部属性进行修改
属性用 final 修饰保证了该属性是只读的,不能修改
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
//....
}更改 String 类数据时,会构造新字符串对象,生成新的 char[] value,通过创建副本对象来避免共享的方式称之为保护性拷贝
无状态:成员变量保存的数据也可以称为状态信息,无状态就是没有成员变量
Servlet 为了保证其线程安全,一般不为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的
ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值
其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号,使用新的原子工具类 AtomicStampedReference
构造方法:
public AtomicStampedReference(V initialRef, int initialStamp):初始值和初始版本号常用 API:
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):期望引用和期望版本号都一致才进行 CAS 修改数据public void set(V newReference, int newStamp):设置值和版本号public V getReference():返回引用的值public int getStamp():返回当前版本号public static void main(String[] args) {
AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);
int startStamp = atomicReference.getStamp();
new Thread(() ->{
int stamp = atomicReference.getStamp();
atomicReference.compareAndSet(100, 101, stamp, stamp + 1);
stamp = atomicReference.getStamp();
atomicReference.compareAndSet(101, 100, stamp, stamp + 1);
},"t1").start();
new Thread(() ->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!atomicReference.compareAndSet(100, 200, startStamp, startStamp + 1)) {
System.out.println(atomicReference.getReference());//100
System.out.println(Thread.currentThread().getName() + "线程修改失败");
}
},"t2").start();
}Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问
Unsafe 类存在 sun.misc 包,其中所有方法都是 native 修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针
模拟实现原子整数:
public static void main(String[] args) {
MyAtomicInteger atomicInteger = new MyAtomicInteger(10);
if (atomicInteger.compareAndSwap(20)) {
System.out.println(atomicInteger.getValue());
}
}
class MyAtomicInteger {
private static final Unsafe UNSAFE;
private static final long VALUE_OFFSET;
private volatile int value;
static {
try {
//Unsafe unsafe = Unsafe.getUnsafe() 这样会报错,需要反射获取
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
// 获取 value 属性的内存地址,value 属性指向该地址,直接设置该地址的值可以修改 value 的值
VALUE_OFFSET = UNSAFE.objectFieldOffset(
MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public MyAtomicInteger(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public boolean compareAndSwap(int update) {
while (true) {
int prev = this.value;
int next = update;
// 当前对象 内存偏移量 期望值 更新值
if (UNSAFE.compareAndSwapInt(this, VALUE_OFFSET, prev, update)) {
System.out.println("CAS 成功");
return true;
}
}
}
}享元模式(Flyweight pattern):用于减少创建对象的数量,以减少内存占用和提高性能,这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式
异步模式:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可将其归类为分工模式,典型实现就是线程池
工作机制:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象
自定义连接池:
public static void main(String[] args) {
Pool pool = new Pool(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Connection con = pool.borrow();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.free(con);
}).start();
}
}
class Pool {
//连接池的大小
private final int poolSize;
//连接对象的数组
private Connection[] connections;
//连接状态数组 0 表示空闲 1 表示繁忙
private AtomicIntegerArray states; //int[] -> AtomicIntegerArray
//构造方法
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i + 1));
}
}
//使用连接
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
System.out.println(Thread.currentThread().getName() + " borrow " + connections[i]);
return connections[i];
}
}
}
//如果没有空闲连接,当前线程等待
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + " wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//归还连接
public void free(Connection con) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == con) {//判断是否是同一个对象
states.set(i, 0);//不用 cas 的原因是只会有一个线程使用该连接
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " free " + con);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
private String name;
//.....
}ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
| 状态名 | 高 3 位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
|---|---|---|---|---|
| RUNNING | 111 | Y | Y | |
| SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余任务 |
| STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
| TERMINATED | 011 | - | - | 终结状态 |
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
// c 为旧值,ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)));
// rs 为高 3 位代表线程池状态,wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)corePoolSize 核心线程数目 (最多保留的线程数)maximumPoolSize 最大线程数目keepAliveTime 生存时间 - 针对救急线程unit 时间单位 - 针对救急线程workQueue 阻塞队列threadFactory 线程工厂 - 可以为线程创建时起个好名字handler 拒绝策略线程池工作流程及拒绝策略
corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 队列排队,直到有空闲的线程。maximumPoolSize - corePoolSize 数目的线程来救急。maximumPoolSize 仍然有新任务,这时会执行拒绝策略。JDK 提供了 4 种实现,其它著名框架也提供了实现: AbortPolicy: 让调用者抛出 RejectedExecutionException 异常,这是默认策略。CallerRunsPolicy: 让调用者运行任务。DiscardPolicy: 放弃本次任务。DiscardOldestPolicy: 放弃队列中最早的任务,本任务取而代之。RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题。corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。代码示例:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}特点:
评价:
代码示例:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}特点:
评价:
代码示例:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}使用场景:
区别:
setCorePoolSize 等方法进行修改。执行任务
void execute(Runnable command);提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;shutdown():停止线程池
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
// 获取线程池全局锁
mainLock.lock();
try {
checkShutdownAccess();
// 设置线程池状态为 SHUTDOWN,如果线程池状态大于 SHUTDOWN,就不会设置直接返回
advanceRunState(SHUTDOWN);
// 中断空闲线程
interruptIdleWorkers();
// 空方法,子类可以扩展
onShutdown();
} finally {
// 释放线程池全局锁
mainLock.unlock();
}
tryTerminate();
}interruptIdleWorkers():shutdown 方法会中断所有空闲线程,根据是否可以获取 AQS 独占锁判断是否处于工作状态。线程之所以空闲是因为阻塞队列没有任务,不会中断正在运行的线程,所以 shutdown 方法会让所有的任务执行完毕
// onlyOne == true 说明只中断一个线程,false 则中断所有线程
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
/ /持有全局锁
mainLock.lock();
try {
// 遍历所有 worker
for (Worker w : workers) {
// 获取当前 worker 的线程
Thread t = w.thread;
// 条件一成立:说明当前迭代的这个线程尚未中断
// 条件二成立:说明【当前 worker 处于空闲状态】,阻塞在 poll 或者 take,因为 worker 执行 task 时是要加锁的
// 每个 worker 有一个独占锁,w.tryLock() 尝试加锁,加锁成功返回 true
if (!t.isInterrupted() && w.tryLock()) {
try {
// 中断线程,处于 queue 阻塞的线程会被唤醒,进入下一次自旋,返回 null,执行退出相逻辑
t.interrupt();
} catch (SecurityException ignore) {
} finally {
// 释放 worker 的独占锁
w.unlock();
}
}
// false,代表中断所有的线程
if (onlyOne)
break;
}
} finally {
// 释放全局锁
mainLock.unlock();
}
}shutdownNow():直接关闭线程池,不会等待任务执行完成
public List<Runnable> shutdownNow() {
// 返回值引用
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
// 获取线程池全局锁
mainLock.lock();
try {
checkShutdownAccess();
// 设置线程池状态为 STOP
advanceRunState(STOP);
// 中断线程池中【所有线程】
interruptWorkers();
// 从阻塞队列中导出未处理的 task
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
// 返回当前任务队列中 未处理的任务。
return tasks;
}tryTerminate():设置为 TERMINATED 状态 if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty)
final void tryTerminate() {
for (;;) {
// 获取 ctl 的值
int c = ctl.get();
// 线程池正常,或者有其他线程执行了状态转换的方法,当前线程直接返回
if (isRunning(c) || runStateAtLeast(c, TIDYING) ||
// 线程池是 SHUTDOWN 并且任务队列不是空,需要去处理队列中的任务
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 执行到这里说明线程池状态为 STOP 或者线程池状态为 SHUTDOWN 并且队列已经是空
// 判断线程池中线程的数量
if (workerCountOf(c) != 0) {
// 【中断一个空闲线程】,在 queue.take() | queue.poll() 阻塞空闲
// 唤醒后的线程会在 getTask() 方法返回 null,
// 执行 processWorkerExit 退出逻辑时会再次调用 tryTerminate() 唤醒下一个空闲线程
interruptIdleWorkers(ONLY_ONE);
return;
}
// 池中的线程数量为 0 来到这里
final ReentrantLock mainLock = this.mainLock;
// 加全局锁
mainLock.lock();
try {
// 设置线程池状态为 TIDYING 状态,线程数量为 0
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 结束线程池
terminated();
} finally {
// 设置线程池状态为 TERMINATED 状态。
ctl.set(ctlOf(TERMINATED, 0));
// 【唤醒所有调用 awaitTermination() 方法的线程】
termination.signalAll();
}
return;
}
} finally {
// 释放线程池全局锁
mainLock.unlock();
}
}
}Timer 实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务
private static void method1() {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println("task 1");
//int i = 1 / 0;//任务一的出错会导致任务二无法执行
Thread.sleep(2000);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("task 2");
}
};
// 使用 timer 添加两个任务,希望它们都在 1s 后执行
// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此任务 1 的延时,影响了任务 2 的执行
timer.schedule(task1, 1000);//17:45:56 c.ThreadPool [Timer-0] - task 1
timer.schedule(task2, 1000);//17:45:58 c.ThreadPool [Timer-0] - task 2
}任务调度线程池 ScheduledThreadPoolExecutor 继承 ThreadPoolExecutor:
构造方法:Executors.newScheduledThreadPool(int corePoolSize)
public ScheduledThreadPoolExecutor(int corePoolSize) {
// 最大线程数固定为 Integer.MAX_VALUE,保活时间 keepAliveTime 固定为 0
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
// 阻塞队列是 DelayedWorkQueue
new DelayedWorkQueue());
}常用 API:
ScheduledFuture<?> schedule(Runnable/Callable<V>, long delay, TimeUnit u):延迟执行任务ScheduledFuture<?> scheduleAtFixedRate(Runnable/Callable<V>, long initialDelay, long period, TimeUnit unit):定时执行周期任务,不考虑执行的耗时,参数为初始延迟时间、间隔时间、单位ScheduledFuture<?> scheduleWithFixedDelay(Runnable/Callable<V>, long initialDelay, long delay, TimeUnit unit):定时执行周期任务,考虑执行的耗时,参数为初始延迟时间、间隔时间、单位基本使用:
延迟任务,但是出现异常并不会在控制台打印,也不会影响其他线程的执行
public static void main(String[] args){
// 线程池大小为 1 时也是串行执行
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,都在 1s 后同时执行
executor.schedule(() -> {
System.out.println("任务 1,执行时间:" + new Date());
//int i = 1 / 0;
try { Thread.sleep(2000); } catch (InterruptedException e) { }
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> {
System.out.println("任务 2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);
}定时任务 scheduleAtFixedRate:一次任务的启动到下一次任务的启动之间只要大于等于间隔时间,抢占到 CPU 就会立即执行
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
System.out.println("start..." + new Date());
pool.scheduleAtFixedRate(() -> {
System.out.println("running..." + new Date());
Thread.sleep(2000);
}, 1, 1, TimeUnit.SECONDS);
}
/*start...Sat Apr 24 18:08:12 CST 2021
running...Sat Apr 24 18:08:13 CST 2021
running...Sat Apr 24 18:08:15 CST 2021
running...Sat Apr 24 18:08:17 CST 2021定时任务 scheduleWithFixedDelay:一次任务的结束到下一次任务的启动之间等于间隔时间,抢占到 CPU 就会立即执行,这个方法才是真正的设置两个任务之间的间隔
public static void main(String[] args){
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
System.out.println("start..." + new Date());
pool.scheduleWithFixedDelay(() -> {
System.out.println("running..." + new Date());
Thread.sleep(2000);
}, 1, 1, TimeUnit.SECONDS);
}
/*start...Sat Apr 24 18:11:41 CST 2021
running...Sat Apr 24 18:11:42 CST 2021
running...Sat Apr 24 18:11:45 CST 2021
running...Sat Apr 24 18:11:48 CST 2021AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器
AQS 用状态属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
AQS 核心思想:
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态
请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中
CLH 是一种基于单向链表的高性能、公平的自旋锁,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

设计原理:
获取锁:
while(state 状态不允许获取) { // tryAcquire(arg)
if(队列中还没有此线程) {
入队并阻塞 park
}
}
当前线程出队释放锁:
if(state 状态允许了) { // tryRelease(arg)
恢复阻塞的线程 (s) unpark
}AbstractQueuedSynchronizer 中 state 设计:
state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态
private volatile int state;state 使用 volatile 修饰配合 cas 保证其修改时的原子性
state 表示线程重入的次数(独占模式)或者剩余许可数(共享模式)
state API:
protected final int getState():获取 state 状态protected final void setState(int newState):设置 state 状态protected final boolean compareAndSetState(int expect,int update):CAS 安全设置 state封装线程的 Node 节点中 waitstate 设计:
使用 volatile 修饰配合 CAS 保证其修改时的原子性
表示 Node 节点的状态,有以下几种状态:
// 默认为 0
volatile int waitStatus;
// 由于超时或中断,此节点被取消,不会再改变状态
static final int CANCELLED = 1;
// 此节点后面的节点已(或即将)被阻止(通过 park),【当前节点在释放或取消时必须唤醒后面的节点】
static final int SIGNAL = -1;
// 此节点当前在条件队列中
static final int CONDITION = -2;
// 将 releaseShared 传播到其他节点
static final int PROPAGATE = -3;阻塞恢复设计:
队列设计:
使用了 FIFO 先入先出队列,并不支持优先级队列,同步队列是双向链表,便于出队入队
// 头结点,指向哑元节点
private transient volatile Node head;
// 阻塞队列的尾节点,阻塞队列不包含头结点,从 head.next → tail 认为是阻塞队列
private transient volatile Node tail;
static final class Node {
// 枚举:共享模式
static final Node SHARED = new Node();
// 枚举:独占模式
static final Node EXCLUSIVE = null;
// node 需要构建成 FIFO 队列,prev 指向前继节点
volatile Node prev;
// next 指向后继节点
volatile Node next;
// 当前 node 封装的线程
volatile Thread thread;
// 条件队列是单向链表,只有后继指针,条件队列使用该属性
Node nextWaiter;
}
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,条件队列是单向链表
public class ConditionObject implements Condition, java.io.Serializable {
// 指向条件队列的第一个 node 节点
private transient Node firstWaiter;
// 指向条件队列的最后一个 node 节点
private transient Node lastWaiter;
}同步器的设计是基于模板方法模式,该模式是基于继承的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码
AbstractQueuedSynchronizer 并重写指定的方法AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:
isHeldExclusively() //该线程是否正在独占资源。只有用到 condition 才需要去实现它
tryAcquire(int) //独占方式。尝试获取资源,成功则返回 true,失败则返回 false
tryRelease(int) //独占方式。尝试释放资源,成功则返回 true,失败则返回 false
tryAcquireShared(int) //共享方式。尝试获取资源。负数表示失败;0 表示成功但没有剩余可用资源;正数表示成功且有剩余资源
tryReleaseShared(int) //共享方式。尝试释放资源,成功则返回 true,失败则返回 falseUnsupportedOperationException自定义一个不可重入锁:
class MyLock implements Lock {
//独占锁 不可重入
class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
// 加上锁 设置 owner 为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override //解锁
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);//volatile 修饰的变量放在后面,防止指令重排
return true;
}
@Override //是否持有独占锁
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition() {
return new ConditionObject();
}
}
private MySync sync = new MySync();
@Override //加锁(不成功进入等待队列等待)
public void lock() {
sync.acquire(1);
}
@Override //加锁 可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override //尝试加锁,尝试一次
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override //尝试加锁,带超时
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override //解锁
public void unlock() {
sync.release(1);
}
@Override //条件变量
public Condition newCondition() {
return sync.newCondition();
}
}ReentrantLock 相对于 synchronized 具备如下特点:
构造方法:ReentrantLock lock = new ReentrantLock();
ReentrantLock 类 API:
public void lock():获得锁
如果锁没有被另一个线程占用,则将锁定计数设置为 1
如果当前线程已经保持锁定,则保持计数增加 1
如果锁被另一个线程保持,则当前线程被禁用线程调度,并且在锁定已被获取之前处于休眠状态
public void unlock():尝试释放锁
基本语法:
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}构造方法:ReentrantLock lock = new ReentrantLock(true)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}ReentrantLock 默认是不公平的:
public ReentrantLock() {
sync = new NonfairSync();
}说明:公平锁一般没有必要,会降低并发度
NonfairSync 继承自 AQS
public void lock() {
sync.lock();
}没有竞争:ExclusiveOwnerThread 属于 Thread-0,state 设置为 1
// ReentrantLock.NonfairSync#lock
final void lock() {
// 用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示【获得了独占锁】
if (compareAndSetState(0, 1))
// 设置当前线程为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//失败进入
}第一个竞争出现:Thread-1 执行,CAS 尝试将 state 由 0 改为 1,结果失败(第一次),进入 acquire 逻辑
// AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
// tryAcquire 尝试获取锁失败时,会调用 addWaiter 将当前线程封装成 node 入队,acquireQueued 阻塞当前线程,
// acquireQueued 返回 true 表示挂起过程中线程被中断唤醒过,false 表示未被中断过
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果线程被中断了逻辑来到这,完成一次真正的打断效果
selfInterrupt();
}
进入 tryAcquire 尝试获取锁逻辑,这时 state 已经是 1,结果仍然失败(第二次),加锁成功有两种情况:
当前 AQS 处于无锁状态
加锁线程就是当前线程,说明发生了锁重入
// ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 抢占成功返回 true,抢占失败返回 false
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// state 值
int c = getState();
// 条件成立说明当前处于【无锁状态】
if (c == 0) {
//如果还没有获得锁,尝试用 cas 获得,这里体现非公平性:不去检查 AQS 队列是否有阻塞线程直接获取锁
if (compareAndSetState(0, acquires)) {
// 获取锁成功设置当前线程为独占锁线程。
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经有线程获得了锁,独占锁线程还是当前线程,表示【发生了锁重入】
else if (current == getExclusiveOwnerThread()) {
// 更新锁重入的值
int nextc = c + acquires;
// 越界判断,当重入的深度很深时,会导致 nextc < 0,int 值达到最大之后再 + 1 变负数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新 state 的值,这里不使用 cas 是因为当前线程正在持有锁,所以这里的操作相当于在一个管程内
setState(nextc);
return true;
}
// 获取失败
return false;
}接下来进入 addWaiter 逻辑,构造 Node 队列(不是阻塞队列),前置条件是当前线程获取锁失败,说明有线程占用了锁
图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
Node 的创建是懒惰的,其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
// AbstractQueuedSynchronizer#addWaiter,返回当前线程的 node 节点
private Node addWaiter(Node mode) {
// 将当前线程关联到一个 Node 对象上,模式为独占模式
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 快速入队,如果 tail 不为 null,说明存在队列
if (pred != null) {
// 将当前节点的前驱节点指向 尾节点
node.prev = pred;
// 通过 cas 将 Node 对象加入 AQS 队列,成为尾节点,【尾插法】
if (compareAndSetTail(pred, node)) {
pred.next = node;// 双向链表
return node;
}
}
// 初始时队列为空,或者 CAS 失败进入这里
enq(node);
return node;
}// AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {
// 自旋入队,必须入队成功才结束循环
for (;;) {
Node t = tail;
// 说明当前锁被占用,且当前线程可能是【第一个获取锁失败】的线程,【还没有建立队列】
if (t == null) {
// 设置一个【哑元节点】,头尾指针都指向该节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 自旋到这,普通入队方式,首先赋值尾节点的前驱节点【尾插法】
node.prev = t;
// 【在设置完尾节点后,才更新的原始尾节点的后继节点,所以此时从前往后遍历会丢失尾节点】
if (compareAndSetTail(t, node)) {
//【此时 t.next = null,并且这里已经 CAS 结束,线程并不是安全的】
t.next = node;
return t; // 返回当前 node 的前驱节点
}
}
}
}
线程节点加入队列成功,进入 AbstractQueuedSynchronizer#acquireQueued 逻辑阻塞线程
acquireQueued 会在一个自旋中不断尝试获得锁,失败后进入 park 阻塞
如果当前线程是在 head 节点后,会再次 tryAcquire 尝试获取锁,state 仍为 1 则失败(第三次)
final boolean acquireQueued(final Node node, int arg) {
// true 表示当前线程抢占锁失败,false 表示成功
boolean failed = true;
try {
// 中断标记,表示当前线程是否被中断
boolean interrupted = false;
for (;;) {
// 获得当前线程节点的前驱节点
final Node p = node.predecessor();
// 前驱节点是 head, FIFO 队列的特性表示轮到当前线程可以去获取锁
if (p == head && tryAcquire(arg)) {
// 获取成功,设置当前线程自己的 node 为 head
setHead(node);
p.next = null; // help GC
// 表示抢占锁成功
failed = false;
// 返回当前线程是否被中断
return interrupted;
}
// 判断是否应当 park,返回 false 后需要新一轮的循环,返回 true 进入条件二阻塞线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 条件二返回结果是当前线程是否被打断,没有被打断返回 false 不进入这里的逻辑
// 【就算被打断了,也会继续循环,并不会返回】
interrupted = true;
}
} finally {
// 【可打断模式下才会进入该逻辑】
if (failed)
cancelAcquire(node);
}
}进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 表示前置节点是个可以唤醒当前节点的节点,返回 true
if (ws == Node.SIGNAL)
return true;
// 前置节点的状态处于取消状态,需要【删除前面所有取消的节点】, 返回到外层循环重试
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 获取到非取消的节点,连接上当前节点
pred.next = node;
// 默认情况下 node 的 waitStatus 是 0,进入这里的逻辑
} else {
// 【设置上一个节点状态为 Node.SIGNAL】,返回外层循环重试
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回不应该 park,再次尝试一次
return false;
}shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次)
当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true
进入 parkAndCheckInterrupt,Thread-1 park(灰色表示)
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// 判断当前线程是否被打断,清除打断标记
return Thread.interrupted();
}再有多个线程经历竞争失败后:

ReentrantLock#unlock:释放锁
public void unlock() {
sync.release(1);
}Thread-0 释放锁,进入 release 流程
进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor
// AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
// 尝试释放锁,tryRelease 返回 true 表示当前线程已经【完全释放锁,重入的释放了】
if (tryRelease(arg)) {
// 队列头节点
Node h = head;
// 头节点什么时候是空?没有发生锁竞争,没有竞争线程创建哑元节点
// 条件成立说明阻塞队列有等待线程,需要唤醒 head 节点后面的线程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}// ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
// 减去释放的值,可能重入
int c = getState() - releases;
// 如果当前线程不是持有锁的线程直接报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否已经完全释放锁
boolean free = false;
// 支持锁重入,只有 state 减为 0, 才完全释放锁成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 当前线程就是持有锁线程,所以可以直接更新锁,不需要使用 CAS
setState(c);
return free;
}进入 AbstractQueuedSynchronizer#unparkSuccessor 方法,唤醒当前节点的后继节点
找到队列中距离 head 最近的一个没取消的 Node,unpark 恢复其运行,本例中即为 Thread-1
回到 Thread-1 的 acquireQueued 流程
private void unparkSuccessor(Node node) {
// 当前节点的状态
int ws = node.waitStatus;
if (ws < 0)
// 【尝试重置状态为 0】,因为当前节点要完成对后续节点的唤醒任务了,不需要 -1 了
compareAndSetWaitStatus(node, ws, 0);
// 找到需要 unpark 的节点,当前节点的下一个
Node s = node.next;
// 已取消的节点不能唤醒,需要找到距离头节点最近的非取消的节点
if (s == null || s.waitStatus > 0) {
s = null;
// AQS 队列【从后至前】找需要 unpark 的节点,直到 t == 当前的 node 为止,找不到就不唤醒了
for (Node t = tail; t != null && t != node; t = t.prev)
// 说明当前线程状态需要被唤醒
if (t.waitStatus <= 0)
// 置换引用
s = t;
}
// 【找到合适的可以被唤醒的 node,则唤醒线程】
if (s != null)
LockSupport.unpark(s.thread);
}从后向前的唤醒的原因:enq 方法中,节点是尾插法,首先赋值的是尾节点的前驱节点,此时前驱节点的 next 并没有指向尾节点,从前遍历会丢失尾节点
唤醒的线程会从 park 位置开始执行,如果加锁成功(没有竞争),会设置

如果这时有其它线程来竞争**(非公平)**,例如这时有 Thread-4 来了并抢占了锁

与非公平锁主要区别在于 tryAcquire 方法:先检查 AQS 队列中是否有前驱节点,没有才去 CAS 竞争
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查 AQS 队列中是否有前驱节点,没有 (false) 才去竞争
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 锁重入
return false;
}
}public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 头尾指向一个节点,链表为空,返回 false
return h != t &&
// 头尾之间有节点,判断头节点的下一个是不是空
// 不是空进入最后的判断,第二个节点的线程是否是本线程,不是返回 true,表示当前节点有前驱节点
((s = h.next) == null || s.thread != Thread.currentThread());
}可重入是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获取这把锁,如果不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁
源码解析参考:nonfairTryAcquire(int acquires)) 和 tryRelease(int releases)
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " execute method2");
} finally {
lock.unlock();
}
}在 Lock 方法加两把锁会是什么情况呢?
public void getLock() {
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
} finally {
lock.unlock();
//lock.unlock();
}
}public void lockInterruptibly():获得可打断的锁
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
System.out.println("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("没有获取到锁,被打断,直接返回");
return;
}
try {
System.out.println("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
Thread.sleep(2000);
System.out.println("主线程进行打断锁");
t1.interrupt();
}不可打断模式:即使它被打断,仍会驻留在 AQS 阻塞队列中,一直要等到获得锁后才能得知自己被打断了
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//阻塞等待
// 如果 acquireQueued 返回 true,打断状态 interrupted = true
selfInterrupt();
}
static void selfInterrupt() {
// 知道自己被打断了,需要重新产生一次中断完成中断效果
Thread.currentThread().interrupt();
}final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 还是需要获得锁后,才能返回打断状态
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
// 条件二中判断当前线程是否被打断,被打断返回 true,设置中断标记为 true,【获取锁后返回】
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// 判断当前线程是否被打断,清除打断标记,被打断返回 true
return Thread.interrupted();
}可打断模式:AbstractQueuedSynchronizer#acquireInterruptibly,被打断后会直接抛出异常
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) {
// 被其他线程打断了直接返回 false
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
// 没获取到锁,进入这里
doAcquireInterruptibly(arg);
}private void doAcquireInterruptibly(int arg) throws InterruptedException {
// 返回封装当前线程的节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//...
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 【在 park 过程中如果被 interrupt 会抛出异常】, 而不会再次进入循环获取锁后才完成打断效果
throw new InterruptedException();
}
} finally {
// 抛出异常前会进入这里
if (failed)
// 取消当前线程的节点
cancelAcquire(node);
}
}// 取消节点出队的逻辑
private void cancelAcquire(Node node) {
// 判空
if (node == null)
return;
// 把当前节点封装的 Thread 置为空
node.thread = null;
// 获取当前取消的 node 的前驱节点
Node pred = node.prev;
// 前驱节点也被取消了,循环找到前面最近的没被取消的节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 获取前驱节点的后继节点,可能是当前 node,也可能是 waitStatus > 0 的节点
Node predNext = pred.next;
// 把当前节点的状态设置为【取消状态 1】
node.waitStatus = Node.CANCELLED;
// 条件成立说明当前节点是尾节点,把当前节点的前驱节点设置为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
// 把前驱节点的后继节点置空,这里直接把所有的取消节点出队
compareAndSetNext(pred, predNext, null);
} else {
// 说明当前节点不是 tail 节点
int ws;
// 条件一成立说明当前节点不是 head.next 节点
if (pred != head &&
// 判断前驱节点的状态是不是 -1,不成立说明前驱状态可能是 0 或者刚被其他线程取消排队了
((ws = pred.waitStatus) == Node.SIGNAL ||
// 如果状态不是 -1,设置前驱节点的状态为 -1
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
// 前驱节点的线程不为 null
pred.thread != null) {
Node next = node.next;
// 当前节点的后继节点是正常节点
if (next != null && next.waitStatus <= 0)
// 把 前驱节点的后继节点 设置为 当前节点的后继节点,【从队列中删除了当前节点】
compareAndSetNext(pred, predNext, next);
} else {
// 当前节点是 head.next 节点,唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}public boolean tryLock():尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列
public boolean tryLock(long timeout, TimeUnit unit):在给定时间内获取锁,获取不到就退出
注意:tryLock 期间也可以被打断
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)) {
System.out.println("获取不到锁");
return;
}
} catch (InterruptedException e) {
System.out.println("被打断,获取不到锁");
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("主线程获取到锁");
t1.start();
Thread.sleep(1000);
try {
System.out.println("主线程释放了锁");
} finally {
lock.unlock();
}
}成员变量:指定超时限制的阈值,小于该值的线程不会被挂起
static final long spinForTimeoutThreshold = 1000L;超时时间设置的小于该值,就会被禁止挂起,因为阻塞在唤醒的成本太高,不如选择自旋空转
tryLock()
public boolean tryLock() {
// 只尝试一次
return sync.nonfairTryAcquire(1);
}tryLock(long timeout, TimeUnit unit)
public final boolean tryAcquireNanos(int arg, long nanosTimeout) {
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire 尝试一次
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}private boolean doAcquireNanos(int arg, long nanosTimeout) {
if (nanosTimeout <= 0L)
return false;
// 获取最后期限的时间戳
final long deadline = System.nanoTime() + nanosTimeout;
//...
try {
for (;;) {
//...
// 计算还需等待的时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L) //时间已到
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果 nanosTimeout 大于该值,才有阻塞的意义,否则直接自旋会好点
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 【被打断会报异常】
if (Thread.interrupted())
throw new InterruptedException();
}
}
}public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");//...
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public void run() {
while (true) {
// 尝试获得左手筷子
if (left.tryLock()) {
try {
// 尝试获得右手筷子
if (right.tryLock()) {
try {
System.out.println("eating...");
Thread.sleep(1000);
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}synchronized 的条件变量,是当条件不满足时进入 WaitSet 等待;ReentrantLock 的条件变量比 synchronized 强大之处在于支持多个条件变量
ReentrantLock 类获取 Condition 对象:public Condition newCondition()
Condition 类 API:
void await():当前线程从运行状态进入等待状态,释放锁void signal():唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁使用流程:
await / signal 前需要获得锁
await 执行后,会释放锁进入 ConditionObject 等待
await 的线程被唤醒去重新竞争 lock 锁
线程在条件队列被打断会抛出中断异常
竞争 lock 锁成功后,从 await 后继续执行
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
//创建一个新的条件变量
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
new Thread(() -> {
try {
lock.lock();
System.out.println("进入等待");
//进入休息室等待
condition1.await();
System.out.println("被唤醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
//叫醒
new Thread(() -> {
try {
lock.lock();
//唤醒
condition2.signal();
} finally {
lock.unlock();
}
}).start();
}总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,每个 Condition 对象都包含一个等待队列
开始 Thread-0 持有锁,调用 await,线程进入 ConditionObject 等待,直到被唤醒或打断,调用 await 方法的线程都是持锁状态的,所以说逻辑里不存在并发
public final void await() throws InterruptedException {
// 判断当前线程是否是中断状态,是就直接给个中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 将调用 await 的线程包装成 Node,添加到条件队列并返回
Node node = addConditionWaiter();
// 完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】
int savedState = fullyRelease(node);
// 设置打断模式为没有被打断,状态码为 0
int interruptMode = 0;
// 如果该节点还没有转移至 AQS 阻塞队列,park 阻塞,等待进入阻塞队列
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 如果被打断,退出等待队列,对应的 node【也会被迁移到阻塞队列】尾部,状态设置为 0
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 逻辑到这说明当前线程退出等待队列,进入【阻塞队列】
// 尝试枪锁,释放了多少锁就【重新获取多少锁】,获取锁成功判断打断模式
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// node 在条件队列时 如果被外部线程中断唤醒,会加入到阻塞队列,但是并未设 nextWaiter = null
if (node.nextWaiter != null)
// 清理条件队列内所有已取消的 Node
unlinkCancelledWaiters();
// 条件成立说明挂起期间发生过中断
if (interruptMode != 0)
// 应用打断模式
reportInterruptAfterWait(interruptMode);
}// 打断模式 - 在退出等待时重新设置打断状态
private static final int REINTERRUPT = 1;
// 打断模式 - 在退出等待时抛出异常
private static final int THROW_IE = -1;
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
private Node addConditionWaiter() {
// 获取当前条件队列的尾节点的引用,保存到局部变量 t 中
Node t = lastWaiter;
// 当前队列中不是空,并且节点的状态不是 CONDITION(-2),说明当前节点发生了中断
if (t != null && t.waitStatus != Node.CONDITION) {
// 清理条件队列内所有已取消的 Node
unlinkCancelledWaiters();
// 清理完成重新获取 尾节点 的引用
t = lastWaiter;
}
// 创建一个关联当前线程的新 node, 设置状态为 CONDITION(-2),添加至队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node; // 空队列直接放在队首【不用 CAS 因为执行线程是持锁线程,并发安全】
else
t.nextWaiter = node; // 非空队列队尾追加
lastWaiter = node; // 更新队尾的引用
return node;
}// 清理条件队列内所有已取消(不是 CONDITION)的 node,【链表删除的逻辑】
private void unlinkCancelledWaiters() {
// 从头节点开始遍历【FIFO】
Node t = firstWaiter;
// 指向正常的 CONDITION 节点
Node trail = null;
// 等待队列不空
while (t != null) {
// 获取当前节点的后继节点
Node next = t.nextWaiter;
// 判断 t 节点是不是 CONDITION 节点,条件队列内不是 CONDITION 就不是正常的
if (t.waitStatus != Node.CONDITION) {
// 不是正常节点,需要 t 与下一个节点断开
t.nextWaiter = null;
// 条件成立说明遍历到的节点还未碰到过正常节点
if (trail == null)
// 更新 firstWaiter 指针为下个节点
firstWaiter = next;
else
// 让上一个正常节点指向 当前取消节点的 下一个节点,【删除非正常的节点】
trail.nextWaiter = next;
// t 是尾节点了,更新 lastWaiter 指向最后一个正常节点
if (next == null)
lastWaiter = trail;
} else {
// trail 指向的是正常节点
trail = t;
}
// 把 t.next 赋值给 t,循环遍历
t = next;
}
}接下来 Thread-0 进入 AQS 的 fullyRelease 流程,释放同步器上的锁
// 线程可能重入,需要将 state 全部释放
final int fullyRelease(Node node) {
// 完全释放锁是否成功,false 代表成功
boolean failed = true;
try {
// 获取当前线程所持有的 state 值总数
int savedState = getState();
// release -> tryRelease 解锁重入锁
if (release(savedState)) {
// 释放成功
failed = false;
// 返回解锁的深度
return savedState;
} else {
// 解锁失败抛出异常
throw new IllegalMonitorStateException();
}
} finally {
// 没有释放成功,将当前 node 设置为取消状态
if (failed)
node.waitStatus = Node.CANCELLED;
}
}fullyRelease 中会 unpark AQS 队列中的下一个节点竞争锁,假设 Thread-1 竞争成功

Thread-0 进入 isOnSyncQueue 逻辑判断节点是否移动到阻塞队列,没有就 park 阻塞 Thread-0
final boolean isOnSyncQueue(Node node) {
// node 的状态是 CONDITION,signal 方法是先修改状态再迁移,所以前驱节点为空证明还【没有完成迁移】
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 说明当前节点已经成功入队到阻塞队列,且当前节点后面已经有其它 node,因为条件队列的 next 指针为 null
if (node.next != null)
return true;
// 说明【可能在阻塞队列,但是是尾节点】
// 从阻塞队列的尾节点开始向前【遍历查找 node】,如果查找到返回 true,查找不到返回 false
return findNodeFromTail(node);
}await 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断:在条件队列被打断的线程需要抛出异常
private int checkInterruptWhileWaiting(Node node) {
// Thread.interrupted() 返回当前线程中断标记位,并且重置当前标记位 为 false
// 如果被中断了,根据是否在条件队列被中断的,设置中断状态码
return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}// 这个方法只有在线程是被打断唤醒时才会调用
final boolean transferAfterCancelledWait(Node node) {
// 条件成立说明当前 node 一定是在条件队列内,因为 signal 迁移节点到阻塞队列时,会将节点的状态修改为 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 把【中断唤醒的 node 加入到阻塞队列中】
enq(node);
// 表示是在条件队列内被中断了,设置为 THROW_IE 为 -1
return true;
}
//执行到这里的情况:
//1.当前 node 已经被外部线程调用 signal 方法将其迁移到 阻塞队列 内了
//2.当前 node 正在被外部线程调用 signal 方法将其迁移至 阻塞队列 进行中状态
// 如果当前线程还没到阻塞队列,一直释放 CPU
while (!isOnSyncQueue(node))
Thread.yield();
// 表示当前节点被中断唤醒时不在条件队列了,设置为 REINTERRUPT 为 1
return false;
}最后开始处理中断状态:
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
// 条件成立说明【在条件队列内发生过中断,此时 await 方法抛出中断异常】
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 条件成立说明【在条件队列外发生的中断,此时设置当前线程的中断标记位为 true】
else if (interruptMode == REINTERRUPT)
// 进行一次自己打断,产生中断的效果
selfInterrupt();
}假设 Thread-1 要来唤醒 Thread-0,进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node,必须持有锁才能唤醒,因此 doSignal 内线程安全
public final void signal() {
// 判断调用 signal 方法的线程是否是独占锁持有线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取条件队列中第一个 Node
Node first = firstWaiter;
// 不为空就将第该节点【迁移到阻塞队列】
if (first != null)
doSignal(first);
}// 唤醒 - 【将没取消的第一个节点转移至 AQS 队列尾部】
private void doSignal(Node first) {
do {
// 成立说明当前节点的下一个节点是 null,当前节点是尾节点了,队列中只有当前一个节点了
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 将等待队列中的 Node 转移至 AQS 队列,不成功且还有节点则继续循环
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
// signalAll() 会调用这个函数,唤醒所有的节点
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
// 唤醒所有的节点,都放到阻塞队列中
} while (first != null);
}执行 transferForSignal,先将节点的 waitStatus 改为 0,然后加入 AQS 阻塞队列尾部,将 Thread-3 的 waitStatus 改为 -1
// 如果节点状态是取消,返回 false 表示转移失败,否则转移成功
final boolean transferForSignal(Node node) {
// CAS 修改当前节点的状态,修改为 0,因为当前节点马上要迁移到阻塞队列了
// 如果状态已经不是 CONDITION, 说明线程被取消(await 释放全部锁失败)或者被中断(可打断 cancelAcquire)
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
// 返回函数调用处继续寻找下一个节点
return false;
// 【先改状态,再进行迁移】
// 将当前 node 入阻塞队列,p 是当前节点在阻塞队列的【前驱节点】
Node p = enq(node);
int ws = p.waitStatus;
// 如果前驱节点被取消或者不能设置状态为 Node.SIGNAL,就 unpark 取消当前节点线程的阻塞状态,
// 让 thread-0 线程竞争锁,重新同步状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
Thread-1 释放锁,进入 unlock 流程
独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁
共享锁:指该锁可以被多个线程锁持有
ReentrantReadWriteLock 其读锁是共享锁,写锁是独占锁
作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
使用规则:
加锁解锁格式:
r.lock();
try {
// 临界区
} finally {
r.unlock();
}读 - 读能共存、读 - 写不能共存、写 - 写不能共存
读锁不支持条件变量
重入时升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写
重入时降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁
w.lock();
try {
r.lock();// 降级为读锁,释放写锁,这样能够让其它线程读取缓存
try {
// ...
} finally{
w.unlock();// 要在写锁释放之前获取读锁
}
} finally{
r.unlock();
}构造方法:
public ReentrantReadWriteLock():默认构造方法,非公平锁public ReentrantReadWriteLock(boolean fair):true 为公平锁常用 API:
public ReentrantReadWriteLock.ReadLock readLock():返回读锁public ReentrantReadWriteLock.WriteLock writeLock():返回写锁public void lock():加锁public void unlock():解锁public boolean tryLock():尝试获取锁读读并发:
public static void main(String[] args) {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock r = rw.readLock();
ReentrantReadWriteLock.WriteLock w = rw.writeLock();
new Thread(() -> {
r.lock();
try {
Thread.sleep(2000);
System.out.println("Thread 1 running " + new Date());
} finally {
r.unlock();
}
},"t1").start();
new Thread(() -> {
r.lock();
try {
Thread.sleep(2000);
System.out.println("Thread 2 running " + new Date());
} finally {
r.unlock();
}
},"t2").start();
}缓存更新时,是先清缓存还是先更新数据库
先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存
先更新据库:可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据
补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

可以使用读写锁进行操作
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个,原理与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
读写锁:
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;构造方法:默认是非公平锁,可以指定参数创建公平锁
public ReentrantReadWriteLock(boolean fair) {
// true 为公平锁
sync = fair ? new FairSync() : new NonfairSync();
// 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}Sync 类的属性:
统计变量:
// 用来移位
static final int SHARED_SHIFT = 16;
// 高 16 位的 1
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 65535,16 个 1,代表写锁的最大重入次数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 低 16 位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;获取读写锁的次数:
// 获取读写锁的读锁分配的总次数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁(独占)锁的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }内部类:
// 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高 16 位记录的是全局范围内所有的读线程获取读锁的总量
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
// 线程安全的存放线程各自的 HoldCounter 对象
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}内部类实例:
// 当前线程持有的可重入读锁的数量,计数为 0 时删除
private transient ThreadLocalHoldCounter readHolds;
// 记录最后一个获取【读锁】线程的 HoldCounter 对象
private transient HoldCounter cachedHoldCounter;首次获取锁:
// 第一个获取读锁的线程
private transient Thread firstReader = null;
// 记录该线程持有的读锁次数(读锁重入次数)
private transient int firstReaderHoldCount;Sync 构造方法:
Sync() {
readHolds = new ThreadLocalHoldCounter();
// 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】
setState(getState());
}t1 线程:w.lock(写锁),成功上锁 state = 0_1
// lock() -> sync.acquire(1);
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
// 尝试获得写锁,获得写锁失败,将当前线程关联到一个 Node 对象上,模式为独占模式
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获得低 16 位,代表写锁的 state 计数
int w = exclusiveCount(c);
// 说明有读锁或者写锁
if (c != 0) {
// c != 0 and w == 0 表示有读锁,【读锁不能升级】,直接返回 false
// w != 0 说明有写锁,写锁的拥有者不是自己,获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 执行到这里只有一种情况:【写锁重入】,所以下面几行代码不存在并发
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入,获得锁成功,没有并发,所以不使用 CAS
setState(c + acquires);
return true;
}
// c == 0,说明没有任何锁,判断写锁是否该阻塞,是 false 就尝试获取锁,失败返回 false
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 获得锁成功,设置锁的持有线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false;
}
// 公平锁会检查 AQS 队列中是否有前驱节点,没有 (false) 才去竞争
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}t2 r.lock(读锁),进入 tryAcquireShared 流程:
返回 -1 表示失败
如果返回 0 表示成功
返回正数表示还有多少后继节点支持共享模式,读写锁返回 1
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
// tryAcquireShared 返回负数,表示获取读锁失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}// 尝试以共享模式获取
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// exclusiveCount(c) 代表低 16 位,写锁的 state,成立说明有线程持有写锁
// 写锁的持有者不是当前线程,则获取读锁失败,【写锁允许降级】
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 高 16 位,代表读锁的 state,共享锁分配出去的总次数
int r = sharedCount(c);
// 读锁是否应该阻塞
if (!readerShouldBlock() && r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // 尝试增加读锁计数
// 加锁成功
// 加锁之前读锁为 0,说明当前线程是第一个读锁线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 第一个读锁线程是自己就发生了读锁重入
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// cachedHoldCounter 设置为当前线程的 holdCounter 对象,即最后一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
// 说明还没设置 rh
if (rh == null || rh.tid != getThreadId(current))
// 获取当前线程的锁重入的对象,赋值给 cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
// 还没重入
else if (rh.count == 0)
readHolds.set(rh);
// 重入 + 1
rh.count++;
}
// 读锁加锁成功
return 1;
}
// 逻辑到这 应该阻塞,或者 cas 加锁失败
// 会不断尝试 for (;;) 获取读锁,执行过程中无阻塞
return fullTryAcquireShared(current);
}
// 非公平锁 readerShouldBlock 偏向写锁一些,看 AQS 阻塞队列中第一个节点是否是写锁,是则阻塞,反之不阻塞
// 防止一直有读锁线程,导致写锁线程饥饿
// true 则该阻塞,false 则不阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}final int fullTryAcquireShared(Thread current) {
// 当前读锁线程持有的读锁次数对象
HoldCounter rh = null;
for (;;) {
int c = getState();
// 说明有线程持有写锁
if (exclusiveCount(c) != 0) {
// 写锁不是自己则获取锁失败
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// 条件成立说明当前线程是 firstReader,当前锁是读忙碌状态,而且当前线程也是读锁重入
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
// 最后一个读锁的 HoldCounter
rh = cachedHoldCounter;
// 说明当前线程也不是最后一个读锁
if (rh == null || rh.tid != getThreadId(current)) {
// 获取当前线程的 HoldCounter
rh = readHolds.get();
// 条件成立说明 HoldCounter 对象是上一步代码新建的
// 当前线程不是锁重入,在 readerShouldBlock() 返回 true 时需要去排队
if (rh.count == 0)
// 防止内存泄漏
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 越界判断
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 读锁加锁,条件内的逻辑与 tryAcquireShared 相同
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}获取读锁失败,进入 sync.doAcquireShared(1) 流程开始阻塞,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上,模式为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱节点就头节点就去尝试获取锁
if (p == head) {
// 再一次尝试获取读锁
int r = tryAcquireShared(arg);
// r >= 0 表示获取成功
if (r >= 0) {
//【这里会设置自己为头节点,唤醒相连的后序的共享节点】
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 是否在获取读锁失败时阻塞 park 当前线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park

这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子

t1 w.unlock,写锁解锁
public void unlock() {
// 释放锁
sync.release(1);
}
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空并且不是等待状态不是 0,唤醒后继的非取消节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因为可重入的原因,写锁计数为 0, 才算释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一
接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,唤醒连续的所有的共享节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置自己为 head 节点
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量),为 0 就没有资源
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取下一个节点
Node s = node.next;
// 如果当前是最后一个节点,或者下一个节点是【等待共享读锁的节点】
if (s == null || s.isShared())
// 唤醒后继节点
doReleaseShared();
}
}private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功,下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// SIGNAL 唤醒后继
if (ws == Node.SIGNAL) {
// 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head,
// 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
if (h == head)
break;
}
}
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}protected final boolean tryReleaseShared(int unused) {
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
// 读锁的计数不会影响其它获取读锁线程,但会影响其它获取写锁线程,计数为 0 才是真正释放
if (compareAndSetState(c, nextc))
// 返回是否已经完全释放了
return nextc == 0;
}
}t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束

StampedLock:读写锁,该类自 JDK 8 加入,是为了进一步优化读性能
特点:
在使用读锁、写锁时都必须配合戳使用
StampedLock 不支持条件变量
StampedLock 不支持重入
基本用法
加解读锁:
long stamp = lock.readLock();
lock.unlockRead(stamp); // 类似于 unpark,解指定的锁加解写锁:
long stamp = lock.writeLock();
lock.unlockWrite(stamp);乐观读,StampedLock 支持 tryOptimisticRead() 方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}提供一个数据容器类内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法:
public static void main(String[] args) throws InterruptedException {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1000);
},"t1").start();
Thread.sleep(500);
new Thread(() -> {
dataContainer.write(1000);
},"t2").start();
}
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public int read(int readTime) throws InterruptedException {
long stamp = lock.tryOptimisticRead();
System.out.println(new Date() + " optimistic read locking" + stamp);
Thread.sleep(readTime);
// 戳有效,直接返回数据
if (lock.validate(stamp)) {
Sout(new Date() + " optimistic read finish..." + stamp);
return data;
}
// 说明其他线程更改了戳,需要锁升级了,从乐观读升级到读锁
System.out.println(new Date() + " updating to read lock" + stamp);
try {
stamp = lock.readLock();
System.out.println(new Date() + " read lock" + stamp);
Thread.sleep(readTime);
System.out.println(new Date() + " read finish..." + stamp);
return data;
} finally {
System.out.println(new Date() + " read unlock " + stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
System.out.println(new Date() + " write lock " + stamp);
try {
Thread.sleep(2000);
this.data = newData;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(new Date() + " write unlock " + stamp);
lock.unlockWrite(stamp);
}
}
}synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行
Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁
构造方法:
public Semaphore(int permits):permits 表示许可线程的数量(state)public Semaphore(int permits, boolean fair):fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程常用 API:
public void acquire():表示获取许可public void release():表示释放许可,acquire() 和 release() 方法之间的代码为同步代码public static void main(String[] args) {
// 1.创建 Semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10 个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 3. 获取许可
semaphore.acquire();
sout(Thread.currentThread().getName() + " running...");
Thread.sleep(1000);
sout(Thread.currentThread().getName() + " end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}加锁流程:
Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源
Sync(int permits) {
setState(permits);
}假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
// acquire() -> sync.acquireSharedInterruptibly(1),可中断
public final void acquireSharedInterruptibly(int arg) {
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取通行证,获取成功返回 >= 0 的值
if (tryAcquireShared(arg) < 0)
// 获取许可证失败,进入阻塞
doAcquireSharedInterruptibly(arg);
}
// tryAcquireShared() -> nonfairTryAcquireShared()
// 非公平,公平锁会在循环内 hasQueuedPredecessors() 方法判断阻塞队列是否有临头节点 (第二个节点)
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取 state,state 这里【表示通行证】
int available = getState();
// 计算当前线程获取通行证完成之后,通行证还剩余数量
int remaining = available - acquires;
// 如果许可已经用完,返回负数,表示获取失败,
if (remaining < 0 ||
// 许可证足够分配的,如果 cas 重试成功,返回正数,表示获取成功
compareAndSetState(available, remaining))
return remaining;
}
}private void doAcquireSharedInterruptibly(int arg) {
// 将调用 Semaphore.aquire 方法的线程,包装成 node 加入到 AQS 的阻塞队列中
final Node node = addWaiter(Node.SHARED);
// 获取标记
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 前驱节点是头节点可以再次获取许可
if (p == head) {
// 再次尝试获取许可,【返回剩余的许可证数量】
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功后本线程出队(AQS), 所在 Node 设置为 head
// r 表示【可用资源数】, 为 0 则不会继续传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 不成功,设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
// 被打断后进入该逻辑
if (failed)
cancelAcquire(node);
}
}private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置自己为 head 节点
setHead(node);
// propagate 表示有【共享资源】(例如共享读锁或信号量)
// head waitStatus == Node.SIGNAL 或 Node.PROPAGATE,doReleaseShared 函数中设置的
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
这时 Thread-4 释放了 permits,状态如下
// release() -> releaseShared()
public final boolean releaseShared(int arg) {
// 尝试释放锁
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 获取当前锁资源的可用许可证数量
int current = getState();
int next = current + releases;
// 索引越界判断
if (next < current)
throw new Error("Maximum permit count exceeded");
// 释放锁
if (compareAndSetState(current, next))
return true;
}
}
private void doReleaseShared() {
// PROPAGATE 详解
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功,下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
}
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,并且 unpark 接下来的共享状态的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
假设存在某次循环中队列里排队的结点情况为 head(-1) → t1(-1) → t2(0),存在将要释放信号量的 T3 和 T4,释放顺序为先 T3 后 T4
// 老版本代码
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
// 有空闲资源
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
// 下一个
if (s == null || s.isShared())
unparkSuccessor(node);
}
}正常流程:
BUG 流程:
更新后流程:
T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从 -1 变为 0
T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,返回值为 0(获取锁成功,但没有剩余资源量)
T1 还没调用 setHeadAndPropagate 方法,T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)
T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置自己为 head 节点
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}// 唤醒
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功,下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成
构造器:
public CountDownLatch(int count):初始化唤醒需要的 down 几步常用 API:
public void await():让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待public void countDown():计数器进行减 1(down 1)应用:同步等待多个 Rest 远程调用结束
// LOL 10 人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
ExecutorService service = Executors.newFixedThreadPool(10);
String[] all = new String[10];
Random random = new Random();
for (int j = 0; j < 10; j++) {
int finalJ = j;//常量
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
Thread.sleep(random.nextInt(100)); //随机休眠
all[finalJ] = i + "%";
System.out.print("\r" + Arrays.toString(all)); // \r代表覆盖
}
latch.countDown();
});
}
latch.await();
System.out.println("\n游戏开始");
service.shutdown();
}
/*
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
游戏开始阻塞等待:
线程调用 await() 等待其他线程完成任务:支持打断
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 判断线程是否被打断,抛出打断异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源
// 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// CountDownLatch.Sync#tryAcquireShared
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
// 将调用 latch.await() 方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 前驱节点时头节点就可以尝试获取锁
if (p == head) {
// 再次尝试获取锁,获取成功返回 1
int r = tryAcquireShared(arg);
if (r >= 0) {
// 获取锁成功,设置当前节点为 head 节点,并且向后传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 阻塞在这里
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
// 阻塞线程被中断后抛出异常,进入取消节点的逻辑
if (failed)
cancelAcquire(node);
}
}获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null
setHead(node);
// propagate = 1,条件一成立
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
// 获取当前节点的后继节点
Node s = node.next;
// 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式
if (s == null || s.isShared())
// 唤醒所有的等待共享锁的节点
doReleaseShared();
}
}计数减一:
线程进入 countDown() 完成计数器减一(释放锁)的操作
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 尝试释放共享锁
if (tryReleaseShared(arg)) {
// 释放锁成功开始唤醒阻塞节点
doReleaseShared();
return true;
}
return false;
}更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
// 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false
if (c == 0)
return false;
// 计数器减一
int nextc = c-1;
if (compareAndSetState(c, nextc))
// 计数器为 0 时返回 true
return nextc == 0;
}
}state = 0 时,当前线程需要执行唤醒阻塞节点的任务
private void doReleaseShared() {
for (;;) {
Node h = head;
// 判断队列是否是空队列
if (h != null && h != tail) {
int ws = h.waitStatus;
// 头节点的状态为 signal,说明后继节点没有被唤醒过
if (ws == Node.SIGNAL) {
// cas 设置头节点的状态为 0,设置失败继续自旋
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的 head,
// 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
if (h == head)
break;
}
}CyclicBarrier:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行
常用方法:
public CyclicBarrier(int parties, Runnable barrierAction):用于在线程到达屏障 parties 时,执行 barrierAction public int await():线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障与 CountDownLatch 的区别:CyclicBarrier 是可以重用的
应用:可以实现多线程中,某个任务在等待其他线程执行完毕以后触发
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
System.out.println("task1 task2 finish...");
});
for (int i = 0; i < 3; i++) { // 循环重用
service.submit(() -> {
System.out.println("task1 begin...");
try {
Thread.sleep(1000);
barrier.await(); // 2 - 1 = 1
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.submit(() -> {
System.out.println("task2 begin...");
try {
Thread.sleep(2000);
barrier.await(); // 1 - 1 = 0
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
service.shutdown();
}全局锁:利用可重入锁实现的工具类
// barrier 实现是依赖于 Condition 条件队列,condition 条件队列必须依赖 lock 才能使用
private final ReentrantLock lock = new ReentrantLock();
// 线程挂起实现使用的 condition 队列,当前代所有线程到位,这个条件队列内的线程才会被唤醒
private final Condition trip = lock.newCondition();线程数量:
private final int parties; // 代表多少个线程到达屏障开始触发线程任务
private int count; // 表示当前“代”还有多少个线程未到位,初始值为 parties当前代中最后一个线程到位后要执行的事件:
private final Runnable barrierCommand;代:
// 表示 barrier 对象当前 代
private Generation generation = new Generation();
private static class Generation {
// 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常
// 且在这一代挂起的线程都会被唤醒,然后抛出 BrokerException 异常。
boolean broken = false;
}构造方法:
public CyclicBarrie(int parties, Runnable barrierAction) {
// 因为小于等于 0 的 barrier 没有任何意义
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
// 可以为 null
this.barrierCommand = barrierAction;
}
await():阻塞等待所有线程到位
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}// timed:表示当前调用 await 方法的线程是否指定了超时时长,如果 true 表示线程是响应超时的
// nanos:线程等待超时时长,单位是纳秒
private int dowait(boolean timed, long nanos) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 获取当前代
final Generation g = generation;
// 【如果当前代是已经被打破状态,则当前调用 await 方法的线程,直接抛出 Broken 异常】
if (g.broken)
throw new BrokenBarrierException();
// 如果当前线程被中断了,则打破当前代,然后当前线程抛出中断异常
if (Thread.interrupted()) {
// 设置当前代的状态为 broken 状态,唤醒在 trip 条件队列内的线程
breakBarrier();
throw new InterruptedException();
}
// 逻辑到这说明,当前线程中断状态是 false,当前代的 broken 为 false(未打破状态)
// 假设 parties 给的是 5,那么 index 对应的值为 4,3,2,1,0
int index = --count;
// 条件成立说明当前线程是最后一个到达 barrier 的线程,【需要开启新代,唤醒阻塞线程】
if (index == 0) {
// 栅栏任务启动标记
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
// 启动触发的任务
command.run();
// run() 未抛出异常的话,启动标记设置为 true
ranAction = true;
// 开启新的一代,这里会【唤醒所有的阻塞队列】
nextGeneration();
// 返回 0 因为当前线程是此代最后一个到达的线程,index == 0
return 0;
} finally {
// 如果 command.run() 执行抛出异常的话,会进入到这里
if (!ranAction)
breakBarrier();
}
}
// 自旋,一直到条件满足、当前代被打破、线程被中断,等待超时
for (;;) {
try {
// 根据是否需要超时等待选择阻塞方法
if (!timed)
// 当前线程释放掉 lock,【进入到 trip 条件队列的尾部挂起自己】,等待被唤醒
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 被中断后来到这里的逻辑
// 当前代没有变化并且没有被打破
if (g == generation && !g.broken) {
// 打破屏障
breakBarrier();
// node 节点在【条件队列】内收到中断信号时 会抛出中断异常
throw ie;
} else {
// 等待过程中代变化了,完成一次自我打断
Thread.currentThread().interrupt();
}
}
// 唤醒后的线程,【判断当前代已经被打破,线程唤醒后依次抛出 BrokenBarrier 异常】
if (g.broken)
throw new BrokenBarrierException();
// 当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑
if (g != generation)
return index;
// 当前线程 trip 中等待超时,然后主动转移到阻塞队列
if (timed && nanos <= 0L) {
breakBarrier();
// 抛出超时异常
throw new TimeoutException();
}
}
} finally {
// 解锁
lock.unlock();
}
}breakBarrier():打破 Barrier 屏障
private void breakBarrier() {
// 将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常
generation.broken = true;
// 重置 count 为 parties
count = parties;
// 将在 trip 条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常
trip.signalAll();
}nextGeneration():开启新的下一代
private void nextGeneration() {
// 将在 trip 条件队列内挂起的线程全部唤醒
trip.signalAll();
// 重置 count 为 parties
count = parties;
// 开启新的一代,使用一个新的 generation 对象,表示新的一代,新的一代和上一代【没有任何关系】
generation = new Generation();
}Exchanger:交换器,是一个用于线程间协作的工具类,用于进行线程间的数据交换
工作流程:两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据
常用方法:
public Exchanger():创建一个新的交换器public V exchange(V x):等待另一个线程到达此交换点public V exchange(V x, long timeout, TimeUnit unit):等待一定的时间public class ExchangerDemo {
public static void main(String[] args) {
// 创建交换对象(信使)
Exchanger<String> exchanger = new Exchanger<>();
new ThreadA(exchanger).start();
new ThreadB(exchanger).start();
}
}
class ThreadA extends Thread{
private Exchanger<String> exchanger();
public ThreadA(Exchanger<String> exchanger){
this.exchanger = exchanger;
}
@Override
public void run() {
try{
sout("线程 A,做好了礼物 A,等待线程 B 送来的礼物 B");
//如果等待了 5s 还没有交换就死亡(抛出异常)!
String s = exchanger.exchange("礼物 A",5,TimeUnit.SECONDS);
sout("线程 A 收到线程 B 的礼物:" + s);
} catch (Exception e) {
System.out.println("线程 A 等待了 5s,没有收到礼物,最终就执行结束了!");
}
}
}
class ThreadB extends Thread{
private Exchanger<String> exchanger;
public ThreadB(Exchanger<String> exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
try {
sout("线程 B,做好了礼物 B,等待线程 A 送来的礼物 A.....");
// 开始交换礼物。参数是送给其他线程的礼物!
sout("线程 B 收到线程 A 的礼物:" + exchanger.exchange("礼物 B"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
评论