Java JUC(二) Synchronized 基本使用与核心原理
随着 Java 多线程开发的引入,程序的执行效率和速度都取得了极大的提升。但此时,多条线程如果同时对一个共享资源进行了非原子性操作则可能会诱发线程安全问题,而线程安全问题将会导致潜在的数据和行为错误。上述概念中包含了三个关键要素即:多线程、共享资源(临界资源)和非原子性操作,这也是线程安全问题产生的根本原因。因此,锁机制的出现就是为了确保多线程对共享资源的安全访问,控制线程之间的协作关系。接下来,本文将介绍最常用的 Synchronized 锁的基本使用与底层原理。
一. Synchronized 基本使用
synchronized
关键字是Java中用于实现线程同步的一种常用机制,其加锁目标是类或实例对象。它的主要作用是确保在同一时刻,只有一个线程可以执行某个方法或代码块,从而避免因多个线程同时访问共享资源时所引发的数据不一致问题。其特点如下:
- 可重入锁:
synchronized
是可重入锁,即线程可以重复获取已持有的synchronized
锁; - 可见性与原子性:
synchronized
既能保证数据的可见性,也能保证修饰方法对数据操作的原子性,但相比volatile
来说其不能禁止指令重排序; - 非公平锁: 获取锁的机会是随机或非顺序的;
1. 基本用法
1.1 修饰实例方法
获取当前对象实例的锁即this
锁,线程想要执行被Synchronized
关键字修饰的普通实例方法,必须先获取当前实例对象的锁资源。
public synchronized void lockInstance() {
System.out.println("锁的是当前对象实例this");
}
注意: 当一个线程正在访问一个被synchronized
修饰的实例方法时,即当一个线程获取了该对象的锁之后,其他线程将无法获取该对象的锁,所以无法访问该对象的其他被synchronized
修饰的对象实例方法。但是如果有其他方法未被synchronized
修饰,又或者其他被synchronized
修饰的是静态方法,则这类方法是可以被其他线程所访问的。
1.2 修饰静态方法
获取当前类对象的锁即class
锁,线程想要执行被Synchronized
关键字修饰的静态方法,必须先获取当前类对象(唯一)的锁资源。
public synchronized static void lockClass() {
System.out.println("锁的是当前类class");
}
注意: 静态 synchronized
方法和非静态 synchronized
方法之间的调用不互斥(如上述);因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问实例 synchronized
方法占用的锁是当前实例对象锁。
1.3 修饰代码块
synchronized修饰代码块(划分细粒度)时,可以对括号内指定的对象实例或类加锁,表示进入同步代码块前要先获得给定对象/类Class的锁资源。其格式如下:
-
synchronized(Object object) {...}
-
synchronized(Class.class) {...}
(1)实例对象锁(Object)
public void lockObjectWithBlock(Object obj) {
synchronized (obj) {
System.out.println("锁的是指定的对象实例obj");
}
}
public void lockObjectWithBlock() {
synchronized (this) {
System.out.println("锁的是当前对象实例this");
}
}
(2)类对象锁(class)
public void lockclassWithBlock() {
synchronized (A.class) {
System.out.println("锁的是指定类A.class");
}
}
2. wait与notify
wait()
和 notify()
相关方法是 Java 的 Object
顶级父类中的方法,它们基于底层monitor
对象监视器实现(后面会说),因此调用线程必须拥有该对象的监视器(即对象锁)才能调用方法,即必须在同步块或同步方法(synchronized
) 中被调用。在应用中,它们常被用于实现多线程间的协作,处理线程间的通信问题。
Type | Method | Description |
---|---|---|
void | wait() | 使当前(持锁)线程进入阻塞等待(进入WAITING 状态),直到另一个线程为此对象调用notify 方法或notifyAll 方法唤醒。注意当前线程必须拥有此对象的监视器(锁)才能调用,并且调用后线程会释放此monitor 对象锁。在使用过程中可能会抛出以下两种异常:- IllegalMonitorStateException :若当前线程未拥有对象的监视器锁;- InterruptedException :若当前线程在唤醒之前被中断,并清除中断状态; |
void | wait(long timeout) | 使当前(持锁)线程进入阻塞等待(进入TIMED_WAITING 状态),直到另一个线程为此对象调用notify 方法或notifyAll 方法唤醒,或等待时间timeout (ms)过期;其他条件与wait 方法保持一致。 |
void | notify() | 唤醒正在等待该对象监视器锁的单个线程,若有多个线程在等待则唤醒哪个线程是不确定的(存在虚假唤醒问题);线程在被唤醒后不会立即获取锁,而是进入等待队列,待对象锁释放之后重新参与锁的竞争(→RUNNABLE →BLOCKED )。注意当前线程同样必须拥有此对象的监视器(锁)才能调用此方法。- IllegalMonitorStateException :若当前线程未拥有对象的监视器锁; |
void | notifyAll() | 唤醒正在等待该对象监视器锁的所有线程。其他条件与notify 方法保持一致。 |
虚假唤醒问题:notify
随机唤醒单个线程,notifyAll
唤醒所有线程;在多线程环境下,二者都会导致某些未达到预期唤醒条件的线程提前苏醒,这就是虚假唤醒问题。要解决虚假唤醒问题,可以通过搭配while
条件循环来判断,其示例如下:
/**
* 场景模拟:奶茶店和咖啡店共用一个窗口(window)出餐,等待顾客点单...
* - 奶茶店(hasTeaWithMilk):顾客需要奶茶,则奶茶店开始工作;
* - 咖啡店(hasCoffee):顾客需要咖啡,则咖啡店开始工作;
*/
public class Test_02 {
// 窗口锁
static final Object window = new Object();
// 奶茶点单标志
static boolean hasTeaWithMilk = false;
// 咖啡点单标志
static boolean hasCoffee = false;
public static void main(String[] args) throws InterruptedException {
// 奶茶店监控线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (window){
System.out.println("[奶茶店] 查看顾客点单情况...");
while (!hasTeaWithMilk){
System.out.println("[奶茶店] 没有点奶茶,继续等待订单...");
try {
window.wait(); // 挂起等待,唤醒后从此继续执行(循环判断下次条件是否成立)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("[奶茶店] 点单没?! " + hasTeaWithMilk);
if(hasTeaWithMilk){
System.out.println("[奶茶店] 开始工作...");
}else{
System.out.println("[奶茶店] 白被叫醒...");
}
}
}
}).start();
Thread.sleep(1000);
// 咖啡店监控线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (window){
System.out.println("[咖啡店] 查看顾客点单情况...");
while (!hasCoffee){
System.out.println("[咖啡店] 没有点咖啡,继续等待订单...");
try {
window.wait(); // 挂起等待,唤醒后从此继续执行(循环判断下次条件是否成立)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("[咖啡店] 点单没?! " + hasCoffee);
if(hasCoffee){
System.out.println("[咖啡店] 开始工作...");
}else{
System.out.println("[咖啡店] 白被叫醒...");
}
}
}
}).start();
Thread.sleep(1000);
// 顾客点单线程: main
synchronized (window){
hasCoffee = true;
System.out.println("[顾客] 点了咖啡!!");
window.notifyAll(); //唤醒全部等待线程
}
}
}
[奶茶店] 查看顾客点单情况...
[奶茶店] 没有点奶茶,继续等待订单...
[咖啡店] 查看顾客点单情况...
[咖啡店] 没有点咖啡,继续等待订单...
[顾客] 点了咖啡!!
[咖啡店] 点单没?! true
[咖啡店] 开始工作...
[奶茶店] 没有点奶茶,继续等待订单...
二. Synchronized 核心原理
早期 Java 版本中,
synchronized
属于重量级锁,其直接关联到 monitor 监视器锁,依赖于底层操作系统的Mutex Lock
来实现,线程之间的切换需要频繁从用户态转换到内核态,成本较高且效率较低。在Java 6之后,JVM 引入了偏向锁和轻量级锁两种锁机制,并进行了大量锁操作的优化(锁消除、锁粗化等),极大提高了锁的获取与释放效率。本节将着重介绍锁升级的实现,其他优化技术感兴趣的同学可以自行了解。注意: 本文涉及的JDK版本是1.8,JVM虚拟机以64位的HotSpot实现为准。
1. 对象内存布局
在 JVM 虚拟机中,Java 对象在内存中的布局可以分为 3 部分区域即:对象头、实例数据、对齐填充;其中:
- 对象头: 存储 MarkWord(标记字段)和类型指针(对象指向其类元数据的指针,标识该对象是哪个类的实例);如果是数组对象,还会存储数组长度(
ArrayLength
); - 实例数据: 非静态成员数据的占用空间(包括父类的所有属性);
- 对齐填充: 为了减少堆内存的碎片空间、提升IO性能,JVM 默认对每个对象做
8byte
的倍数填充;
在对象头部分,MarkWord(标记字段)用于存储对象运行时的基本信息,包括HashCode、GC分代年龄以及锁状态标志位等锁信息;而 synchronized 关键字的实现就与 MarkWord 的结构密不可分。64位 JVM 的Mark Word 组成示例如下:
2. 锁优化与锁升级
锁升级是结合实际经验针对不同竞争条件下的一种优化,synchronized
锁主要包括四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,随着线程竞争的激烈程度,锁会逐渐升级以应对不同的场景。注意:锁升级一般是单向的(由低级到高级),不可降级。
2.1 无锁状态
JDK1.6之后,JVM默认开启两个参数:-XX:+UseBiasedLocking
(表示启用偏向锁)、-XX:BiasedLockingStartupDelay=4000
(表示JVM启动后延迟4000ms初始化匿名偏向锁);也就是说,在JVM启动后的前4000ms内创建的对象都是无锁状态,而匿名偏向锁初始化后创建的所有对象的对象头都为匿名偏向锁状态,其中匿名偏向锁是指锁状态为偏向锁时,偏向线程ID为空的特殊状态。注意该场景需要关注以下两点:
- 为什么要有匿名偏向锁: 按照我个人的理解,偏向锁出现根本目的是为了减小在大部分场景下单个线程重复进入同步代码块时的开销,而偏向锁状态根据偏向线程ID字段是否为空又能天然表示当前是否存在偏向两个情况,无锁状态的存在此时多少显得没有什么意义。除此之外,提前进入匿名偏向锁可以简化锁升级过程、优化单线程访问下的同步开销;
- 为什么要有启动延迟: JVM 启动时默认会进行大量sync操作,而偏向锁在面临同步和竞争时并不是都有利的,此时会发生大量的锁撤销和锁升级操作,极大降低JVM的启动效率;
综上所述,默认情况下JVM启动后4000ms内的普通无锁对象,以及4000ms后的匿名偏向锁对象都属于意义上的"无锁状态"。对于无锁状态的锁对象,如果有竞争发生则会直接进入轻量级锁;而匿名偏向锁对象则会尝试进行非匿名偏向锁的替换,也就是说只有匿名偏向的对象才能进入偏向锁模式。
2.2 偏向锁状态
在大多数实际情况下(经验之谈),锁不仅很少存在多线程竞争,反而由同一线程重复获得的情况是最频繁的,因此为了减少先前单线程获取锁的代价与开销,从而引入了偏向锁机制。由上可知,偏向锁状态的应用场景是单线程访问临界区资源或进入同步代码块。
偏向锁的核心思想是在偏向锁状态下,此时MarkWord
结构中的偏向线程ID字段用于存储当前持有锁的单一线程,当线程请求获取锁时只需比较偏向线程ID字段是否与当前线程一致即可,这样同一线程重复获取锁时无需进行任何额外操作即可进入同步代码(记录重入次数),从而省去了大量有关锁申请的开销;注意:偏向锁申请后不会主动释放(即使线程退出了同步代码块甚至线程已终止),除非进入了偏向锁撤销/升级。 但是若线程请求获取锁时发现偏向线程不一致,则其处理过程如下:
- S1: 判断锁是否仍处于偏向模式(101),若否则进入轻量级锁升级逻辑;
- S2: 判断是否为匿名偏向锁或epoch过期,若是则尝试通过一次
CAS
进行偏向ID替换; - S3: 若
CAS
失败或偏向非当前线程,都说明出现了两个线程对锁的竞争,此时不管先前持有锁的线程是否已退出同步代码块或是否线程已终止,都会进入偏向锁的撤销和轻量级锁的升级逻辑(也有文章说此时会判断先前线程是否已退出同步代码块或已终止并尝试使用CAS替换以保持偏向锁状态,但我并不认同,实验如下);
//JOL库:用来查看对象头信息
import org.openjdk.jol.info.ClassLayout;
public class Test_01 {
public static void main(String[] args) throws InterruptedException {
// 等待JVM延迟启动
Thread.sleep(5000);
// 1.初始化匿名偏向锁对象 101
Object obj = new Object();
System.out.println("匿名偏向锁: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 2.单线程的偏向锁状态 101
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
System.out.println("t1 获得锁: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 已退出同步代码块: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
});
t1.start();
// 3.偏向锁不释放 101
t1.join();
Thread.sleep(1000);
System.out.println("t1 线程执行已结束: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 4.偏向锁升级轻量级锁 00
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
System.out.println("t2 获得锁: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2 已退出同步代码块: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
});
t2.start();
// 轻量级锁释放回到无锁状态 001
t2.join();
Thread.sleep(1000);
System.out.println("t2 线程执行已结束: ");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
/**
* JOL输出的对象头信息:
* 1.[object header: mark]: MarkWord(64位),默认输出是16进制的,需要转换成64位的2进制来看(后三位为锁标志位)
* 2.[object header: class]: 类元指针
* 3.[object alignment gap]: 对齐填充
*/
匿名偏向锁:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t1 获得锁:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000020cdf13d005 (biased: 0x000000008337c4f4; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t1 已退出同步代码块:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000020cdf13d005 (biased: 0x000000008337c4f4; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t1 线程执行已结束:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000020cdf13d005 (biased: 0x000000008337c4f4; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t2 获得锁:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000c07ceff0b8 (thin lock: 0x000000c07ceff0b8)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t2 已退出同步代码块:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t2 线程执行已结束:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
最后,偏向锁并不都有利,其只适用于单个线程重入的场景,而对于多线程竞争的场景下只会增大开销。原因是:偏向锁的撤销需要进入safepoint
(全局安全点),该过程开销较大。需要进入safepoint
是由于,偏向锁的撤销需要到其他线程的栈帧中遍历寻找锁对象的lockRecord
;而在非safepoint,栈帧是动态变化的,修改动态数据会引入更多的问题。目前看来,偏向锁存在的价值是为历史遗留的Collection类(如Vector和HashTable等)做优化,迟早药丸。因此,Java 15中默认不开启偏向锁。
2.3 轻量级锁状态
由经验数据可知:“对于绝大部分的锁,在整个同步周期内都不存在竞争”,因此轻量级锁的应用场景是多线程交替访问临界区资源或进入同步代码块。在轻量级锁状态下,作为锁的对象头中的MarkWord用于存储指向持有锁的线程栈中的锁记录(Lock Record)的指针,那么什么是LockRecord
?LockRecord
是每个线程私有的数据结构,用于存储锁对象头中MardWord
的拷贝(Displaced MarkWord
)以及指向锁对象的指针,但一般LockRecord
只有在轻量级线程状态才会真正发挥作用。
LockRecord
的作用主要有两方面:一是保存元数据,即由于MarkWord
的空间有限,随着对象状态的改变MarkWord
的结构也会发生变化,这样原本存储在对象头里的一些信息(如HashCode
、分代年龄等)就无法存储,为了保证数据不丢失,就会拷贝原MarkWord
(无锁状态下)到线程栈中(称为Displaced Mark Word
),并通过指针指向;二是实现轻量级锁的重入,即轻量级锁重入次数=栈中该锁对象的LockRecord
数量,其中只有第一个无锁状态下的LockRecord
会存储完整的Displaced Mark Word
拷贝,其他重入LockRecord
的Displaced Mark Word=NULL
,只存指向锁对象的指针,用于标识重入次数。
举个例子
synchronized(obj){synchronized(obj){synchronized(obj){ ... }}}
该代码块会生成三个lockrecord,其中只有第一个lockrecord的Displaced Mark Word里有完整拷贝信息,其displaced_header低三位 = 001,即无锁状态;其他后面两个lockrecord的Displaced Mark Word = NULL,仅存储锁对象指针。
那么为什么轻量锁用lockrecord数量来统计重入次数?个人猜测是因为锁对象markword中放不下。
轻量级锁的核心思想是:在LockRecord
拷贝完成后,线程通过CAS尝试将MarkWord的锁记录指针修改为指向自己(线程)的锁记录,然后将lockrecord的owner指向锁对象,若修改成功则当前线程将获得轻量级锁。进入轻量级锁有两种情况:
- 无锁状态进入轻量级锁: 如上,使用CAS操作尝试将对象头的Markword更新为指向Lock Record的指针;
- 偏向锁状态进入轻量级锁: 偏向锁撤销时,如果偏向线程仍在持有锁,则直接进入轻量级锁形式(safepoint无需CAS);
注意: 轻量级锁在退出同步代码块时,会主动释放锁并回到无锁状态,此时会检查markword中的lockrecord指针是否指向自己(获得锁的线程lockrecord),然后使用原子的CAS将Displaced Mark Word替换回对象头,如果成功则表示没有竞争发生,如果替换失败则会升级为重量级锁。
最后,关于轻量级锁进入重量级锁的时的行为,目前有两种主流观点,一是轻量级锁状态在竞争锁时会进行自旋,超过一定次数/自适应自旋则进入重量级锁升级;二是轻量级锁状态不会进行自旋,一旦发现多线程同时竞争就进入重量级锁升级,但反而在重量级锁初期会先尝试自适应自旋来竞争锁。两种观点的文章如下:
- 轻量级锁状态自旋:浅析synchronized锁升级的原理与实现
- 重量级锁状态自旋:Java锁与线程的那些事 (youzan.com)
但不管哪一种观点,两者都是在用户态进行自适应自旋,以尽可能减少同步操作带来的开销,没有太多本质上的区别。综上所述,轻量级锁适用于线程的交替执行场景:线程A进入轻量级锁,退出同步代码块并释放锁,会将锁对象恢复为无锁状态;线程B再进入锁,发现为无锁状态,会CAS
尝试获取该锁对象的轻量级锁。
2.4 重量级锁状态
重量级锁的应用场景是多个线程同时访问临界区资源或进入同步代码块。在重量级锁状态下,作为锁的对象头中的MarkWord存储的是指向监视器monitor
对象的指针;在HotSpot
虚拟机中,monitor
是由ObjectMonitor
实现的,其主要数据结构如下:
ObjectMonitor() {
_header = NULL; //markOop对象头
_count = 0; //记录个数
_waiters = 0, //等待线程数
_recursions = 0; //重入次数
_object = NULL; //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL; //指向获得ObjectMonitor对象的线程或基础锁
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL;
FreeNext = NULL;
_EntryList = NULL; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ; // _owner is (Thread *) vs SP/BasicLock
_previous_owner_tid = 0; // 监视器前一个拥有者线程的ID
}
重量级锁的核心思想是:给每个锁对象分配并绑定一个底层的monitor监视器对象。在其具体实现中,一个ObjectMonitor对象包括两个同步队列(_cxq 和 _EntryList) ,以及一个等待队列 _WaitSet。cxq、EntryList 、WaitSet都是由ObjectWaiter构成的链表结构(每个等待锁的线程都会被封装成ObjectWaiter对象)。重量级锁竞争时,请求线程会首先尝试获取锁,若加锁失败则当前节点加入cxq队列中阻塞等待(BLOCKED);当线程释放锁时(包括执行完同步代码块或调用了wait方法),则会从cxq或EntryList队列中按照策略(由QMode
参数决定)选择一个线程unpark唤醒,重新参与锁的竞争(非公平锁,不一定成功)。当线程获取到对象的monitor后会进入Owner区域,并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count+1,表示获锁成功,从而继续执行后续的同步代码。
如果线程获得锁后调用
Object#wait
方法,则会将线程加入到WaitSet中,从而进入WAITING或TIMED_WAITING状态阻塞(该过程会释放锁并通过底层的park
方法挂起线程);当被Object#notify
唤醒后,会将WaitSet列表中的一个ObjectWaiter节点从WaitSet移动到cxq或EntryList队列中去,重新等待锁的等待竞争(notify并不意味着立即获取锁,仍需进入队列重新竞争)。需要注意的是,当调用一个锁对象的wait或notify方法时,若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁,因为这两个方法都依赖于monitor实现。
综上所述,重量级锁会将线程放进等待队列,并交由操作系统进行调度,涉及用户态和内核态的转变,因此效率很低(相当于单线程执行)。除此之外,我们还需要注意一下在线程生命周期转换部分:BLOCKED
状态的线程一定处于entryList或cxq中阻塞,而处于WAITING
和TIMED WAITING
的线程,可能是由于执行了sleep或park进入该状态(不一定在waitSet中)。
三. 实战应用
1. 同步之保护性暂停模式
保护性暂停模式(Guarded Suspension)是一种并发设计模式,常用于实现线程间对通信结果的同步等待,属于同步模式。保护性暂停模式的核心思想是以GuardedObject
作为中间锁对象,通过wait¬ify
方法实现线程的等待与唤醒,JDK中join
、Future
的底层实现就是基于该设计模式。在我来看,该模式最大的作用就是实现解耦。带有超时时间的保护性暂停模式实现如下:
- 未超时获取到结果: 被唤醒后由于
result!=null
,退出循环并返回结果; - 超时未获取到结果: 超时唤醒后发现
waitTime==0
,break
退出循环并结束; - 虚假唤醒: 被唤醒后由于
result==null
,继续等待剩余waitTime=timeout-passedTime
时间;
// 测试类
public class Test_03 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 等待结果...");
Object result = guardedObject.get(3000);
System.out.println("t1 获取结果 = " + result);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2 设置结果...");
guardedObject.complete(null); // 模拟虚假唤醒
System.out.println("t2 设置完成...");
}
}).start();
}
}
//保护性暂停对象: 同步等待过程的解耦
class GuardedObject{
// 目标结果
private Object result = null;
/**
* 获取结果方法
* @param timeout 最大等待时间(ms)
*/
public Object get(long timeout){
synchronized (this) {
long begin = System.currentTimeMillis(); // 等待开始时间
long passedTime = 0; // 已经等待时间
while (result == null){
long waitTime = timeout - passedTime; // 解决虚假唤醒问题
if(waitTime <= 0){
break;
}
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - begin;
}
return result;
}
}
/**
* 设置结果方法
* @param result 结果
*/
public void complete(Object result) {
synchronized (this) {
this.result = result;
this.notifyAll(); // 唤醒所有等待线程
}
}
}
2. 异步之生产者/消费者模式
以消息队列为例,生产者/消费者模式用于实现线程之间多对多的资源平衡问题,并且支持异步消费/生产。其特点如下:
- 当生产者向消息队列中放入消息时,向其他等待的消费者发送可消费通知;当消息队列满后,生产者停止生产并等待消费。
- 当消费者从消息队列中消费消息时,向其他等待的生产者发送可生产通知;当消息队列空后,消费者停止消费并等待生产。
// 测试类
public class test_04 {
public static void main(String[] args) {
MessageQueue messageQueue = new MessageQueue(2);
for(int i = 0;i < 3;i++){
int id = i;
new Thread(()->{
messageQueue.put(new Message(id,"消息"+id));
},"生产者"+i).start();
}
new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message message = messageQueue.take();
}
},"消费者").start();
}
}
// 消息类: 不可变数据
final class Message{
private int id;
private Object value;
public Message(int id, Object value){
this.id = id;
this.value = value;
}
public int getId(){
return id;
}
public Object getValue(){
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
// 消息队列类
class MessageQueue{
// 存储消息的集合
private LinkedList<Message> valueList = new LinkedList<>();
// 队列容量
private int capacity;
public MessageQueue(int capacity){
this.capacity = capacity;
}
// 获取/消费消息方法
public Message take(){
synchronized (valueList){
// 检查队列是否为空,为空则等待生产
while(valueList.isEmpty()){
try {
System.out.println(Thread.currentThread().getName() + " 队列为空,消费者线程等待...");
valueList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费头部消息
Message message = valueList.removeFirst();
System.out.println(Thread.currentThread().getName() + " 已消费消息: " + message);
// 通知生产者线程
valueList.notifyAll();
return message;
}
}
// 存入/生产消息方法
public void put(Message message){
synchronized (valueList){
// 检查队列是否已满,已满则等待消费
while(valueList.size() == capacity){
try {
System.out.println(Thread.currentThread().getName() + " 队列已满,生产者线程等待...");
valueList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产消息插入队尾
valueList.addLast(message);
System.out.println(Thread.currentThread().getName() + " 已生产消息: " + message);
// 通知消费者线程
valueList.notifyAll();
}
}
}
生产者0 已生产消息: Message{id=0, value=消息0}
生产者2 已生产消息: Message{id=2, value=消息2}
生产者1 队列已满,生产者线程等待...
消费者 已消费消息: Message{id=0, value=消息0}
生产者1 已生产消息: Message{id=1, value=消息1}
消费者 已消费消息: Message{id=2, value=消息2}
消费者 已消费消息: Message{id=1, value=消息1}
消费者 队列为空,消费者线程等待...