深入剖析 Java synchronized
在 Java 的进阶学习中,多线程环境下对 临界区代码的同步处理 是一个非常重要的内容。其中,synchronized 关键字作为实现临界资源同步最直接、最简洁的方式,扮演着关键角色
尽管 synchronized 使用简单,但它的底层实现却涉及 JVM 锁机制、对象头、Monitor、锁升级 等复杂概念。很多人仅仅停留在“加锁能保证线程安全”的层面,却不知道:
- 为什么
synchronized既能修饰方法,也能修饰代码块? - 锁到底存储在对象的哪个部分?
synchronized源码是如何实现的- 重量级锁 加锁解锁流程
- 锁升级是如何优化性能的?
本文将从 使用方式、字节码分析、对象头结构、锁升级过程、性能优化 等多个维度,深入解析 synchronized 的底层原理,帮助你在高并发场景下写出更高效的代码,本文大量参考了 B 站视频 UP 黑马程序员满老师。
临界区
临界区 是指一段可能被多个线程同时执行的代码,而这段代码访问了 共享资源(变量、集合或者文件),由于多个线程同时访问这些共享资源,可能会导致数据不一致或程序行为异常,例如下面这个非常经典的例子
两个线程同时对 count 进行累加和累减操作,最终结果应当为 0,但由于 count++ 和 count-- 操作不是原子性的而导致最后结果五花八门
这就是最常见的线程安全问题,使用 synchronzed 就能规避这个问题🙋
synchronized 的三种实现
synchronized 可以用于 方法 或 代码块 中,其主要有以下三种典型使用方式:
修饰代码块
在 synchronized 中使用同一个 锁对象 锁住临界区的代码
▼java复制代码private static final Object LOCK = new Object(); Thread thread1 = new Thread(() -> { synchronized (LOCK) { for (int i = 0; i < LOOP_NUM; i++) { count++; } } }); Thread thread2 = new Thread(() -> { synchronized (LOCK) { for (int i = 0; i < LOOP_NUM; i++) { count--; } } });
�这是最灵活的方法,通过锁住具体的代码可以实现更细粒度的锁控制
修饰实例方法
synchronized 也可以加载实例方法上,相当于对方法整体加锁,加锁对象为 当前实例对象
▼java复制代码class ThreadSafeCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public int getCount() { return this.count; } } ThreadSafeCounter threadSafeCounter = new ThreadSafeCounter(); Thread thread1 = new Thread(() -> { for (int i = 0; i < LOOP_NUM; i++) { threadSafeCounter.increment(); } }) Thread thread2 = new Thread(() -> { for (int i = 0; i < LOOP_NUM; i++) { threadSafeCounter.decrement(); } });
使用的时候创建出 实例对象,等同于 synchronized(this),当然创建出两个实例,两个实例之间的对象加锁是互不干扰的
修饰静态方法
synchronized 关键字还可以加在 静态方法 上,加锁对象为当前 class 类
▼java复制代码private static synchronized void increment() { for (int i = 0; i < LOOP_NUM; i++) { count--; } }
例如下面两个方法,加锁的对象就是 SynchronizedLearning.class
▼java复制代码Thread thread2 = new Thread(SynchronizedLearning::increment); Thread thread1 = new Thread(SynchronizedLearning::decrement);
字节码分析
对字节码的分析能让我们对 java 程序更加了解,首先对 synchronized 加锁的代码块进行分析,通过 javap 工具可以查看字节码的信息
▼java复制代码private static void lambda$main$0(); descriptor: ()V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC Code: stack=2, locals=3, args_size=0 0: getstatic #17 // Field LOCK:Ljava/lang/Object; 3: dup 4: astore_0 5: monitorenter 6: iconst_0 7: istore_1 8: iload_1 9: ldc #19 // int 100000 11: if_icmpge 28 14: getstatic #13 // Field count:I 17: iconst_1 18: iadd 19: putstatic #13 // Field count:I 22: iinc 1, 1 25: goto 8 28: aload_0 29: monitorexit 30: goto 38 33: astore_2 34: aload_0 35: monitorexit 36: aload_2 37: athrow 38: return Exception table: from to target type 6 30 33 any 33 36 33 any
上面的字节码对应了上面 thread1 的代码部分,我们主要对加锁的字节码进行重点分析
▼java复制代码0: getstatic
用于获取 锁对象,是本类中的静态字段 LOCK,并压入操作数栈中
▼java复制代码3: dup
dup 命令用于 复制操作数栈顶部元素(也就是 LOCK 对象)原因是接下来两个地方要用这个锁对象
monitorenter加锁操作需要使用- 如果发生异常,
monitorexit需要再次使用到
▼java复制代码5: monitorenter
🔒 加锁指令!为什么是 monitor,后面会提到 synchronized 在不同线程竞争情况下,采用操作系统中 monitor 实现
- JVM 会尝试获取对象锁(即对 LOCK 对象加锁)
- 如果其它线程已持有锁,则会阻塞直到获取锁
从这里开始进入 synchronized 控制的代码块区域
▼java复制代码29: monitorexit 30: goto 38 ...... 38: return
这里 monitorexit 是去释放锁,执行完代码块的任务,然后 38 行🔚 方法执行结束
但是可以看到在方法执行结束之前,还有部分字节码
▼java复制代码33: astore_2 34: aload_0 35: monitorexit 36: aload_2 37: athrow
这部分就是 JVM 高明的地方,考虑到开发者都没考虑到的地方,这里要结合下面的 Exception table
▼java复制代码Exception table: from to target type 6 30 33 any 33 36 33 any
这张表里记录了所有可能 出现异常的代码块部分 和 处理部分
这里第一行 6 - 30 行代码是整个加锁和处理部分,如果这部分代码出现了任何异常,字节码都会跳到 33 行字节码开始执行,也就是上面部分代码
这里就做了一件事情,monitorexit 去释放 synchronized 加的锁,防止出现这部分代码一直占住锁
这部分是 JVM 给我们生成的,开发者不需要去考虑 synchronized 代码块中出现异常时 锁释放 问题,这也正是 JVM 厉害👍的地方
以上是 synchronized 针对代码块进行加锁,如果是针对 实例方法 或者 静态方法 加锁,对应的方法字节码如下
▼java复制代码public synchronized void increment(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field count:I 5: iconst_1 6: iadd 7: putfield #2 // Field count:I 10: return LineNumberTable: line 89: 0 line 90: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lioc/ThreadSafeCounter;
其中 flags 中会记录 ACC_SYNCHRONIZED,标志这个方法被 synchronized 关键词修饰,如果是 静态方法(ACC_STATIC),JVM 就会锁住 class,如果是 实例方法,则锁住对象本身(this)
▼java复制代码private static void lambda$main$0(ioc.ThreadSafeCounter); descriptor: (Lioc/ThreadSafeCounter;)V flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Mark Word
在 JVM 中,每个对象在堆内存中的存储结构分为 对象头、实例数据、对齐填充 三部分:
- 对象头 是 JVM 为每个对象预留的元信息区域,最重要的内容有两部分
- Mark Word,后面将会重点介绍
- Klass Pointer,是指向 元数据(Class Metadata),找到自己的类信息,包括字段定义、方法表、接口、父类、GC信息等等
- 实例对象,用于存储类的字段信息
- 对齐填充(padding),为了提高内存访问效率,JVM 要求对象大小是 8 字节的整数倍,若对象头 + 实例数据加起来不是 8 的倍数,就会 自动添加填充字节
Mark Word 是 synchronized 实现锁的关键,存储了当前锁的类型和一些对象状态,下面是 64 位 JVM 中 Mark Word 的结构,在不同锁状态下 Mark Word 存储内容也不同
▼java复制代码|--------------------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |--------------------------------------------------------------------------------|--------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | lock: 01 | Normal | |--------------------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock: 01 | Biased | |--------------------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | lock: 00 | Lightweight Locked | |--------------------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | lock: 10 | Heavyweight Locked | |--------------------------------------------------------------------------------|--------------------| | | lock: 11 | Marked for GC | |--------------------------------------------------------------------------------|--------------------|
其中 lock 位表示了当前对象的不同加锁状态:
01:无锁 / 偏向锁00:轻量级锁10:重量级锁11:GC 标记
接下来结合 synchronized 锁升级的过程来详细讲述 Mark Word 中的内容
synchronized 锁升级
这里为了能让 Java 输出 Mark Word 数据,使用了 JOL 工具来查看当前对象 Mark Word 信息,但 JOL 生成对象头信息过多,不方便我们查看,这里我自己写了一个工具类将对象头中 Mark Word 信息展示出来,并转成 二进制,方便我们查看
▼xml复制代码<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.17</version> </dependency>
在 JDK 15 之后废弃⚠️偏向锁,考虑到目前 JDK 都已经升级到 24版本,不对偏向锁做介绍
无锁状态
对象刚创建时,属于无锁状态
▼java复制代码Object lock = new Object(); System.out.println(MarkWordUtils.getMarkWordBinary(lock));
通过 JOL 工具在控制台打印出了 lock 对象头信息
▼java复制代码0000000000000000000000000000000000000000000000000000000000000001
如上面介绍的 lock 为 01,biased_lock 为 0,表示这个对象处于 无锁状态
轻量级锁
1)加锁场景
当两个或者多个线程 交替 执行同步代码块,注意 没有竞争!没有竞争!没有竞争!
▼java复制代码System.out.println("锁创建时 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t\t锁状态:" + MarkWordUtils.getLock(lock)); Thread t1 = new Thread(() -> { synchronized (lock) { System.out.println("t1 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); t1.start(); t1.join(); // 阻塞等待,保证 t1 线程执行完成再执行 t2 线程 Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); t2.start(); t2.join(); System.out.println("锁全部释放 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock));
�在上面的代码中,t1 和 t2 虽然都是使用 lock 作为锁对象,但是两个线程并 不是同时加锁,轮流使用 lock 对象,下面是输出内容

可以看到当 t1 进入同步代码块的时候,lock 的 Mark Word 后两位变成了 00,即锁当前状态为轻量级锁
2)源码分析
这里查看 synchronized 加锁的 jdk 17 源码 来分析轻量级加锁过程,代码位置在 src/hotspot/share/runtime/synchronizer.hpp 中
▼cpp复制代码void ObjectSynchronizer::enter(Handle obj, BasicLock*lock, JavaThread*current) { // ... 前面代码省略 ... markWord mark = obj -> mark(); assert (!mark.has_bias_pattern(),"should not see bias pattern here"); if (mark.is_neutral()) { lock -> set_displaced_header(mark); if (mark == obj()->cas_set_mark(markWord::from_pointer (lock), mark)){ return; } // ... 后续代码省略 ... } // ... 后续代码省略 ... }
这里只要先看 if 部分代码即可,ObjectSynchronizer::enter 方法对应上面字节码中的 monitorenter 命令,现在逐行来看加锁过程
mark.is_neutral()用来确定 对象未被锁定(无锁状态)lock->set_displaced_header(mark):保存原始 Mark Word,lock是当前线程栈上分配的 锁记录(Lock Record),set_displaced_header将 mark 设置到 锁记录 中
轻量级锁记录 源码在同目录下 basicLock 中
▼cpp复制代码class BasicLock { // ... 前面代码省略 ... private: volatile markWord _displaced_header; // ... 后续代码省略 ... } class BasicObjectLock { // ... 前面代码省略 ... private: BasicLock _lock; // the lock, must be double word aligned oop _obj; // object holds the lock; // ... 后续代码省略 ... };
锁记录中主要存储了两个重要信息
BasicLock _lock用于保存 锁对象的原始 Mark Wordoop _obj用于指向 加锁对象
再回到轻量级锁加锁源码中,obj()->cas_set_mark(markWord::from_pointer(lock), mark) 中 原子交换 当前对象 Mark Word 和 锁记录指针进行交换
from_pointer(lock)获取轻量级锁的 地址指针obj()->cas_set_mark是一个 **CAS(Compare-And-Sweep)**操作,简化成obj->cas(new, old),CAS 操作保证了操作的 原子性- 比较 old 值和 obj->mark 值是否相同,都是原始对象 Mark Word
- new 值是轻量级锁记录 地址指针 + 00(轻量级锁 Mark Word 标识)
- new 和 old 值进行交换,解锁的时候能根据地址解锁
- lock 对象 Mark Word 最后两位变成
00,轻量级锁加成功

3)重入锁
当前对象已经加了轻量级锁,并且是自己再次加锁。允许 同一个线程 多次获取 同一把锁,这种锁被称为「可重入的(reentrant)」
▼java复制代码new Thread(() -> { synchronized (lock) { synchronized (lock) { } } }).start();
对于已经 持有轻量级锁 的当前线程再次加锁,往栈桢中添加 对象存储 null 的锁记录,源码也很好理解
▼java复制代码else if (mark.has_locker() && current->is_lock_owned((address)mark.locker())) { assert(lock != mark.locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark().value(), "don't relock with same BasicLock"); lock->set_displaced_header(markWord::from_pointer(NULL)); return; }
if 条件判断当前是重入轻量级锁,添加锁记录代码为 lock->set_displaced_header(markWord::from_pointer(NULL));,往栈桢中添加存储为 NULL 锁记录
3)解锁步骤
源码中解锁方法为 ObjectSynchronizer::exit,对应字节码中的 monitorexit 命令,下面是轻量级锁的解锁部分源码
▼c复制代码void ObjectSynchronizer::exit(oop object, BasicLock*lock, JavaThread*current) { markWord mark = object -> mark(); markWord dhw = lock -> displaced_header(); // 判断是否为重入的锁 if (dhw.value() == 0) { return; } // 轻量级锁解锁流程 if (mark == markWord::from_pointer (lock)){ if (object -> cas_set_mark(dhw, mark) == mark) { return; } } // 省略后续代码... }
首先先获取加 锁对象的 Mark Word 和 锁记录头中存储的信息,这里对象信息已经交换,mark 存储的是 锁记录地址指针,dhw 存储原始对象 Mark Word
dhw.value() == 0 上面提到,如果是同一个线程重入的锁的锁记录头中存储的信息为 NULL,则不用执行任何操作,直接返回
当 mark == markWord::from_pointer(lock) 为 true 时,说明
- Mark Word 最后两位是
00(轻量级锁标识) - 锁被当前线程持有
markWord::from_pointer(lock) 为 false 时说明:
- 当前线程不持有轻量级锁
- 当前锁对象已经成为重量级锁
这是就会都会进入锁膨胀,并执行重量级锁解锁步骤
因此可以确认 当前线程仍持有该对象的轻量级锁,进入解锁流程,object -> cas_set_mark(dhw, mark) == mark 再把锁记录中的原始 Mark Word 通过 CAS 交换到锁对象的 Mark Word 中
- CAS 执行成功,成功解锁轻量级锁,对象重新变为
无锁状态 - CAS 解锁失败,进入重量级锁解锁流程
重量级锁
如果轻量级锁加锁失败,分成下面几种情况:
- 默认关闭轻量级锁加锁步骤
- CAS 竞争失败
- 其他线程已经占用轻量级锁
此时都会升级成为 重量级锁,进入重量级锁的加锁流程(锁膨胀),交由 内核 中的 Monitor 管理
▼c复制代码// An async deflation can race after the inflate() call and before // enter() can make the ObjectMonitor busy. enter() returns false if // we have lost the race to async deflation and we simply try again. while (true) { ObjectMonitor* monitor = inflate(current, obj(), inflate_cause_monitor_enter); if (monitor->enter(current)) { return; } }
1)Moniter 对象
当前有多个线程进行竞争时,就会触发 锁膨胀,从轻量级锁升级为 重量级锁,重量级锁会由 Moniter 对象进行管理
▼java复制代码System.out.println("锁创建时 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t\t锁状态:" + MarkWordUtils.getLock(lock)); Thread t1 = new Thread(() -> { synchronized (lock) { TimeUnit.MICROSECONDS.sleep(1); System.out.println("t1 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2 占用锁 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock)); } }); // 启动线程,阻塞等待,省略代码 System.out.println("锁全部释放 Mark Word: " + MarkWordUtils.getMarkWordBinary(lock) + "\t锁状态:" + MarkWordUtils.getLock(lock));
这时两个对象发生争抢,lock 对象的 Mark Word 最后两位为 10,锁状态为 重量级锁

这里锁释放了不会恢复 无锁状态,依旧会处于重量级锁状态
Monitor 对象负责管理重量级锁,主要由三部分组成:WaitSet,EntryList 和 Owner

- Owner:当前持有线程,唯一能执行同步代码块,唯一能调用
wait()方法 - EntryList:「想要锁但还没得到」的线程队列,线程状态为
BLOCKED - WaitSet:「主动放弃 Owner,等待条件」的线程(调用
wait()方法),等待notify唤醒
2)锁膨胀
锁膨胀总共分为 4 种情况
- Inflated:已经膨胀完毕
- Stack-locked:从轻量级锁开始膨胀
- Inflating:膨胀中
- Neutral:从无锁状态开始膨胀
▼cpp复制代码for (;;) { const markWord mark = object->mark(); assert(!mark.has_bias_pattern(), "invariant"); // The mark can be in one of the following states: // * Inflated - just return // * Stack-locked - coerce it to inflated // * INFLATING - busy wait for conversion to complete // * Neutral - aggressively inflate the object. // * BIASED - Illegal. We should never see this }
锁膨胀在一个 死循环 中,每一次都会获取对象的 Mark Word,判断当前状态,其中从无锁对象膨胀和从轻量级锁开始膨胀逻辑相似,只介绍从轻量级锁开始膨胀
- Inflated 膨胀完成
▼c复制代码// CASE: inflated if (mark.has_monitor()) { ObjectMonitor* inf = mark.monitor(); markWord dmw = inf->header(); return inf; }
如果当前已经是重量级锁,直接获取 Moniter 对象返回
- Stack-lock 从轻量级锁开始膨胀
▼c复制代码if (mark.has_locker()) { // 1. 创建 moniter 对象 ObjectMonitor* m = new ObjectMonitor(object); // 2. 将当前对象头设置为全0,为膨胀中标识 markWord cmp = object->cas_set_mark(markWord::INFLATING(), mark); // 3. CAS 失败,删除 moniter 对象 if (cmp != mark) { delete m; continue; // Interference -- just retry } // 4.1 获取原始 mark word 值 markWord dmw = mark.displaced_mark_helper(); // 4.2 将原始 Mark word 存入 moniter 中 m->set_header(dmw); // 4.3 设置 mointer 对持锁线程 m->set_owner_from(NULL, mark.locker()); // 4.4 锁对象头设置新的 mark word object->release_set_mark(markWord::encode(m)); _in_use_list.add(m); return m; }
mark.has_locker() 用来判断当前对象是否为轻量级锁,创建一个新的 moniter 对象,通过 CAS 并将当前对象 mark word 全部置为 0,标志 膨胀中 状态,关键是下面设置重量级锁的 4 个步骤
mark.displaced_mark_helper()获取原始的 mark word 的值m->set_header(dmw)把原始 mark word 值存储在moniter中m->set_owner_from(NULL, mark.locker());设置moniter的 Owner,即当前持重量级锁的线程object->release_set_mark(markWord::encode(m))将 monitor 的地址 encode 编码 +10存入锁对象 mark word 中
经过上面操作,会保证只有一个线程针对 lock 锁对象加 重量级锁 成功
- inflating 膨胀中
当前处于膨胀中的状态代码很好理解,read_stable_mark 方法等待一会
▼cpp复制代码if (mark == markWord::INFLATING()) { read_stable_mark(object); continue; }
所有处于膨胀中的线程都是上一步 CAS 加锁竞争失败的,会等待真正加锁的线程加锁成功,在这里 循环等待
3)误区:轻量级锁真的有自旋吗?
网上有些说法说 轻量级锁 膨胀为 重量级锁 时会去尝试自旋等待轻量级锁释放,自旋 10 次失败升级为重量级锁

甚至问 AI 有时也会得到这样的结果,还有一个合理的理由:避免升级成重量级锁的性能开销。但实际上却是如此吗?
这个说法完全是错误的。上面关于轻量级锁膨胀为重量级锁的源码已经剖析过了,当有线程发生竞争的时候,就会立刻执行锁膨胀流程,不会去自旋 10 次。而上面唯一有循环等待的过程只有当线程处于 **膨胀中 **时会去循环等待加锁线程成功
4)重量级锁自旋
当对象已经升级成为 重量级锁 后,其他线程再来争抢重量级锁失败后,不会直接进入 Moniter 对象中的 EntryList,也就是并不会直接进入 BLOCKED 堵塞状态,而会选择 自旋 的方式尝试等待获取 重量级锁
重量级锁自旋获取锁的逻辑在源码中为 ObjectMonitor::TrySpin 方法,其中自旋方式分为:
- 固定自旋 1000 次(jdk 1.6 后被禁用)
- 自适应自旋
这里源码过长,挑重要的部分片段来解释 自适应自旋 获取锁的逻辑,与自适应自旋相关参数
▼c复制代码// 自旋次数上限 int ObjectMonitor::Knob_SpinLimit = 5000; // 成功奖励 static int Knob_Bonus = 100; // 失败惩罚 static int Knob_Penalty = 200; // 自旋次数下限 static int Knob_Poverty = 1000; // 预自旋次数 static int Knob_PreSpin = 10;
首先会进入 预自旋 流程,自旋 11 次
▼cpp复制代码for (ctr = Knob_PreSpin + 1; --ctr >= 0;) { // 进行自旋获取锁... }
- 自旋成功获取锁,直接返回
- 自旋失败,进入下面 完整自旋
进入 完整自旋 逻辑,当前自旋次数由全局变量 _SpinDuration (初始 5000 次)记录
- 自旋失败,下次
-200次,最少减为0(0 表示下次完成预自旋直接进入阻塞) - 自旋成功,下次
+200次,至少加为1000(1000 值是指从 0 开始累加的话,直接从 1000 开始累加)
当自旋全部失败后,失败线程就会进入重量级锁加锁流程
6)_cxq 队列
这里的加锁流程是指当前 Owner 已经被占用,其他线程的「挣扎过程」,当失败线程执行完自旋获取锁的流程后,会进入 ObjectMonitor::EnterI 方法
- 线程使用
TATAS (Test-And-Test-And-Set)尝试 快速获取锁,即使上面 5000 次自旋失败了,线程还会努力 挣扎一次,如果成功了,直接将 Owner 设置为当前线程,直接返回
▼c复制代码// Try the lock - TATAS if (TryLock (current) > 0) { // ... return; }
- 再次进入 自适应自旋尝试,因为上一次失败了次数
-200,如果上次自旋次数是 5000 次,那这次会自旋 4800 次
▼c复制代码if (TrySpin(current) > 0) { return; }
- 自旋还是失败后,会讲当前线程加入
Monitor对象中的 _cxq 栈 中
▼c复制代码ObjectWaiter node(current); node.TState = ObjectWaiter::TS_CXQ; // CAS操作 将当前线程加入_cxq队列 for (;;) { node._next = nxt = _cxq; if (Atomic::cmpxchg(&_cxq, nxt, &node) == nxt) break; // 如果CAS失败,重试获取锁 if (TryLock(current) > 0) return; }
创建 ObjectWaiter 节点表示当前线程,压入栈(LIFO 后进先出),并设置一个 责任线程
▼c复制代码if (nxt == NULL && _EntryList == NULL) { Atomic::replace_if_null(&_Responsible, current); }
如果没有责任线程且队列为空,设置当前线程为 责任线程。所有加入 _cxq 栈中的线程都会使用 park() 命令让自己 阻塞
- 设置责任线程定期唤醒,非责任线程一直阻塞
▼c复制代码for (;;) { if (TryLock(current) > 0) break; // 责任线程使用定时park,非责任线程无限期park if (_Responsible == current) { current->_ParkEvent->park((jlong)recheckInterval); recheckInterval = MIN2(recheckInterval*8, MAX_RECHECK_INTERVAL); } else { current->_ParkEvent->park(); } // 唤醒后再次尝试获取锁 if (TryLock(current) > 0) break; }
通过设置责任线程的机制,能够防止线程永久挂起,并且节省了性能的开销
7)解锁步骤
- 若当前线程为轻量级锁,但调用的是
ObjectMonitor::exit重锁解锁方法,会先加重锁,再解锁 - 若为重入锁(重量级锁重入的时候会对
Monitor中的_recursions累加),则计数-1直接返回
重量级锁正常解锁过程,首先讲 Owner 设置为 NULL,顺序执行下来会出现 4 中不同情况
- _cxq 和 EntryList 全部为空,直接返回
▼c复制代码// 没有等待线程,直接返回 if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0) { return; }
- 若 EntryList 为空,_cxq 有数据,则将 _cxq 列表全部转移到 EntryList 中
▼c复制代码for (;;) { assert(w != NULL, "Invariant"); ObjectWaiter* u = Atomic::cmpxchg(&_cxq, w, (ObjectWaiter*)NULL); if (u == w) break; w = u; }
通过使用 Atomic::cmpxchg 保证队列转移的原子性
▼c复制代码转换前 _cxq (LIFO 单向链表): +---+ +---+ +---+ | C |--->| B |--->| A |---> NULL +---+ +---+ +---+ 转换步骤: 1. 断开 _cxq 链接 2. 遍历链表建立双向链接 转换后 _EntryList (FIFO 双向链表): +---+ +---+ +---+ NULL <--- | A | <->| B | <->| C | ---> NULL +---+ +---+ +---+ ^ | _EntryList 指向这里
- 当前 EntryList 不为空,从队列获取 头节点,
unpark()唤醒,返回
▼c复制代码w = _EntryList; if (w != NULL) { ExitEpilog(current, w); return; }
w 是 EntryList 的头结点,这里调用 ExitPilog 方法来唤醒 w 线程,主要有四个步骤
▼c复制代码// 1. 设置继承者 _succ = Wakee->_thread; // 2. 释放锁 release_clear_owner(current); // 3. 内存屏障保证操作顺序 OrderAccess::fence(); // 4. 唤醒线程 Trigger->unpark();
OrderAccess::fence() 是一个 全内存屏障(full memory barrier),用于保证了 2 步骤一定先于 4 步骤执行,即肯定先释放锁再唤醒线程
最后这里用一张流程图来简单梳理 重量级锁的解锁流程

8)误区:锁释放,会从 EntryList 中随机挑选一个线程唤醒吗?
很多八股文都会说当 Owner 释放了重量级锁后,会随机从 EntryList 中挑选一个线程唤醒获得锁,这的是这样吗?
结论显然是错误的,我们上面已经剖析过源码,竞争的线程会加入 _cxq 栈中的(LIFO,先进后出),然后再将 _cxq 内容加入 EntryList 队列中(FIFO,先进先出),因此唤醒顺序一定是确认的,下面的示例代码将证明这一点
▼java复制代码final Object lock = new Object(); new Thread(() -> { synchronized (lock) { try { System.out.println("t0 加锁成功"); System.in.read(); // 阻塞 t0 } catch (IOException e) { throw new RuntimeException(e); } } }, "t0").start(); TimeUnit.MICROSECONDS.sleep(100); new Thread(() -> { synchronized (lock) { System.out.println("t1 获取锁"); } }, "t1").start(); TimeUnit.MICROSECONDS.sleep(100); new Thread(() -> { synchronized (lock) { System.out.println("t2 获取锁"); } }, "t2").start(); TimeUnit.MICROSECONDS.sleep(100); new Thread(() -> { synchronized (lock) { System.out.println("t3 获取锁"); } }, "t3").start();
t0 线程优先获取锁,并阻塞,让 t1,t2,t3 先后加锁,最后查看获取锁的先后情况

最后执行结果说明了顺序一定是如下图所示,EntryList 是一个 先进先出 的队列,t3 最先被唤醒,并非网上说的随机挑选一个线程获取锁

9)为什么有了 EntryList 还需要 _cxq
逻辑上,我们只需要一个队列就可以管理阻塞的线程,然后唤醒。但实现上,我们把这个队列分成了两部分:_cxq 和 EntryList,主要为了避免 ABA 问题 和 惊群效应,以及 兼顾了吞度量和公平性
- ABA 问题
ABA 问题 是并发编程中的一个经典问题,主要出现在特别是在使用 CAS(Compare-And-Swap) 操作时可能发生
对于共享变量 x = a, 执行 cas(a, b) 是原子性的将 x 的值交换成 b,并且会把交换的旧值返回
- 当前 x 为 a,则交换成功,x = b
- 当前 x 被其他线程修改成了 c,则交换失败,本线程需要重试

当 CAS 操作结合 栈 时,就会引发 ABA 问题:,例如下面的示例

这个图已经基本反映了 ABA 问题 的核心逻辑:一个线程在用 CAS 操作时,只判断了“值有没有变化”,没考虑“值是否曾经被改过”
- _cxq 是 LIFO(栈)结构,插入简单,不容易出现 ABA
- 只有 JVM 自己在合适时机把 _cxq 中的线程“批量转移”到 EntryList → 所以可以集中加锁、避免高频竞 → 避免 ABA 问题

- 惊群效应
惊群效应 指的是当 一个资源释放时,所有等待的线程都被唤醒并竞争,但最终只有一个线程能成功获取锁,其他线程又得重新挂起,导致 大量无意义的上下文切换和CPU争抢
通过 _cxq + EntryList 每次只唤醒一个线程(EntryList) 减少竞争,提高吞吐量
- 兼顾吞吐量和公平性
_cxq 相当于缓存了最近竞争的线程,提高了整体的 吞吐量
EntryList 采用 FIFO 顺序可以一定的 公平性,防止线程 饿死(长时间获取不到锁)
