锁机制
锁的分类与特点
锁的分类是对锁的多种评价标准。一把锁可同时符合多个分类标准,如 ReentrantLock 既是可中断锁,又是可重入锁。
锁的 7 大分类
- 偏向锁/轻量级锁/重量级锁;
- 可重入锁/非可重入锁;
- 共享锁/独占锁;
- 公平锁/非公平锁;
- 悲观锁/乐观锁;
- 自旋锁/非自旋锁;
- 可中断锁/不可中断锁。
偏向锁/轻量级锁/重量级锁
特指 synchronized 锁的状态,通过对象头中的 mark word 表明。
- 偏向锁:若锁自始至终不存在竞争,则无需上锁,只打标记。对象初始化后尚无线程获取锁时为可偏向;第一个线程访问并获取锁时被记录下来,后续若获取锁的线程正是偏向锁拥有者,可直接获得,开销小。
- 轻量级锁:synchronized 中代码多为线程交替执行而非同时执行,无实际竞争或竞争时间短,用 CAS 即可解决,无需完全互斥的重量级锁。当锁原为偏向锁时被另一线程访问,说明存在竞争,偏向锁升级为轻量级锁,线程通过自旋尝试获取锁而不阻塞。
- 重量级锁:互斥锁,利用操作系统同步机制实现,开销较大。多线程存在实际竞争且竞争时间长时,轻量级锁不满足需求,锁膨胀为重量级锁,获取不到锁的线程进入阻塞状态。

锁升级路径:无锁→偏向锁→轻量级锁→重量级锁。
偏向锁性能最好,避免执行 CAS;轻量级锁用自旋和 CAS 避免重量级锁的线程阻塞与唤醒,性能中等;重量级锁阻塞获取不到锁的线程,性能最差。
重要版本变更(JEP 374,JDK 15+):由于偏向锁在竞争场景下会引入额外的 revoke 停顿(stop-the-world),且收益在当今硬件与锁竞争模式下微乎其微,JDK 15 起默认禁用偏向锁,相关命令行选项
-XX:+UseBiasedLocking已废弃,JDK 21 中偏向锁已无实际应用。现代 JDK(15+)的锁升级路径实际为:无锁→轻量级锁→重量级锁(偏向锁默认为关,升级路径中不再包含偏向锁)。学习偏向锁的核心价值在于理解 JVM 锁机制演进,而非在生产中依赖它。
可重入锁/非可重入锁
可重入锁:线程已持有锁时,可不释放锁再次获取。非可重入锁:线程即使持有锁,想再次获取也必须先释放锁。
典型可重入锁是 ReentrantLock(reentrant 即可重入),是 Lock 接口最主要的实现类。
共享锁/独占锁
共享锁:同一把锁可被多个线程同时获得。独占锁:只能同时被一个线程获得。
读写锁最好诠释二者理念:读锁是共享锁,可被多个线程同时持有;写锁是独占锁,最多同时被一个线程持有。
公平锁/非公平锁
公平锁:拿不到锁的线程都进入等待队列排队,等待时间长的线程优先获得锁(先来先得)。非公平锁:在一定情况下忽略已在排队的线程,发生插队现象。
悲观锁/乐观锁
悲观锁:获取资源前必须先拿到锁达到"独占"状态,操作资源时其他线程不能拿到锁故不能影响。乐观锁:不要求在获取资源前拿锁,也不锁住资源,利用 CAS 理念在不独占资源的情况下完成资源修改。
自旋锁/非自旋锁
自旋锁:拿不到锁时不直接阻塞或释放 CPU,而是循环不停尝试获取锁("自旋")。非自旋锁:没有自旋过程,拿不到锁直接放弃或进行其他处理(排队、阻塞等)。
可中断锁/不可中断锁
Java 中 synchronized 关键字修饰的锁是不可中断锁,线程申请锁后只能等到拿到锁才能进行其他逻辑。ReentrantLock 是可中断锁,可用 lockInterruptibly 在获取锁过程中中断,中断后可去做其他事情。
悲观锁与乐观锁
悲观锁和乐观锁从是否锁住资源的角度进行分类。
悲观锁
悲观锁认为不锁住资源,其他线程就会争抢并造成数据错误。为确保结果正确,每次获取并修改数据时都把数据锁住,让其他线程无法访问。

线程 A、B 使用悲观锁,尝试获取同步资源时必须先拿到锁。

线程 A 拿到锁并操作同步资源时,线程 B 必须等待。

线程 A 执行完毕后,CPU 才唤醒等待锁的线程 B 再次尝试获取锁。

线程 B 获取到锁后才能操作同步资源。
乐观锁
乐观锁认为自己操作资源时不会有其他线程干扰,故不锁住被操作对象。为确保数据正确性,更新前对比修改期间数据是否被其他线程修改过:
- 未被修改,说明只有自己在操作,可正常修改数据。
- 数据与最初拿到的不一致,说明其他线程修改过,则放弃本次修改,选择报错、重试等策略。

乐观锁一般用 CAS 算法实现。线程 A 使用乐观锁时,无需提前获取锁,可直接读取同步资源并在线程内计算。

计算完毕、准备更新同步资源前,先判断资源是否被其他线程修改过。

未被修改(与最初数据一致),则更新同步资源完成修改。

已被其他线程修改(数据与最初不一致),则不继续修改,根据业务逻辑选择报错或重试。
悲观锁与乐观锁是广义思想,可应用于其他领域,如数据库。
典型案例
- 悲观锁:synchronized 关键字和 Lock 接口。以
ReentrantLock为例,lock()加锁、unlock()解锁,处理资源前必须先加锁并拿到锁,处理完后再解锁。 - 乐观锁:原子类。如
AtomicInteger更新数据时使用乐观锁思想,多个线程可同时操作同一原子变量。 - 数据库:
select for update为悲观锁,提交前不允许第三方修改数据,有性能损耗,高并发下不可取。可用 version 版本字段实现乐观锁:获取及修改数据不加锁,更新时检查版本号与获取时是否一致,一致则更新,不一致则重新获取、计算、再尝试更新。
SQL 示例(取出数据时 version 为 1):
UPDATE student
SET
name = ‘小李’,
version= 2
WHERE id= 100
AND version= 1
悲观锁与乐观锁的权衡
"悲观锁性能不如乐观锁,应尽量避免"的说法不成立。
- 悲观锁让拿不到锁的线程阻塞,但开销固定。原始开销高于乐观锁,但一劳永逸,即使一直拿不到锁也不额外增加开销。
- 乐观锁初始开销小,但若一直拿不到锁、并发量大、竞争激烈导致不停重试,消耗资源越来越多,开销可能超过悲观锁。
同一把锁在不同场景下效果可能完全不同,需将合适的锁用到合适场景。
两种锁各自的使用场景
- 悲观锁:适合并发写入多、临界区代码复杂、竞争激烈等场景,可避免大量无用的反复尝试。
- 乐观锁:适合大部分是读取、少部分是修改的场景,也适合读写多但并发不激烈的场景。不加锁的特点可大幅提升性能。
synchronized 与 monitor 锁
获取和释放 monitor 锁的时机
synchronized 修饰的代码块或方法,同一时刻最多只有一个线程运行,背后利用 monitor 锁实现。
每个 Java 对象都可作为实现同步的锁(内置锁或 monitor 锁)。获得 monitor 锁的唯一途径是进入由其保护的同步代码块或同步方法。线程进入被 synchronized 保护的代码块前自动获取锁,退出时(无论是正常路径还是抛异常)都自动释放锁。
synchronized 修饰方法的例子:
public synchronized void method() {
method body
}
等价伪代码:
public void method() {
this.intrinsicLock.lock();
try{
method body
}
finally {
this.intrinsicLock.unlock();
}
}
进入方法后立刻加内置锁,用 try 保护方法体,finally 释放锁。这里的 intrinsicLock 即 monitor 锁。
用 javap 命令查看反汇编的结果
JVM 实现 synchronized 方法和代码块的细节不同,分别说明。
同步代码块
public class SynTest {
public void synBlock() {
synchronized (this) {
System.out.println("lagou");
}
}
}
用 cd 切到 SynTest.java 所在路径,执行 javac SynTest.java 生成 SynTest.class,再执行 javap -verbose SynTest.class 查看反汇编内容。关键信息如下:
public void synBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String lagou
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
synchronized 代码块多了 monitorenter 和 monitorexit 指令(第 3、13、19 行)。一个 monitorenter 对应两个 monitorexit:JVM 需保证每个 monitorenter 有对应 monitorexit。monitorenter 插入同步代码块开始处,monitorexit 插入方法正常结束处和异常处两处,保证异常时也能释放锁。
可将执行 monitorenter 理解为加锁、monitorexit 为释放锁。每个对象维护记录被锁次数的计数器,未被锁定的对象计数器为 0。
- monitorenter:执行线程尝试获得 monitor 所有权,发生以下三种情况之一:
- a. monitor 计数为 0,线程获得 monitor 并将计数设为 1,成为该 monitor 的所有者。
- b. 线程已拥有该 monitor,重新进入并累加计数。
- c. 其他线程已拥有该 monitor,当前线程被阻塞,直到计数变为 0(monitor 释放)后再尝试获取。
- monitorexit:将 monitor 计数器减 1,直到减为 0,代表 monitor 已被释放、无任何线程拥有,即解锁。其他等待的线程此时可再次尝试获取所有权。
同步方法
同步代码块用 monitorenter/monitorexit 指令实现。synchronized 方法不依赖这两条指令,反汇编后与普通方法大部分相同,区别在于方法带 ACC_SYNCHRONIZED flag 修饰符。
同步方法代码:
public synchronized void synMethod() {
}
对应反汇编指令:
public synchronized void synMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 16: 0
被 synchronized 修饰的方法带 ACC_SYNCHRONIZED 标志。线程访问该方法时先检查是否有该标志,有则先获得 monitor 锁再执行,方法执行后释放锁。其他方面与同步代码块类似,其他线程请求执行时也会因无法获得 monitor 锁而阻塞。
综上:获取和释放 monitor 锁的时机、synchronized 的等价代码、同步代码块用 monitorenter/monitorexit 指令实现、同步方法用 flags 实现。
synchronized 与 Lock 的选择
相同点
重点讲 3 个大的相同点。
-
都用于保护资源线程安全。这是基本作用。
-
都保证可见性。对 synchronized,线程 A 进入 synchronized 块前或块内的操作,对后续获得同一 monitor 锁的线程 B 可见,体现 happens-before 针对 synchronized 的原则。

Lock 与 synchronized 一样保证可见性,解锁前的所有操作对加锁后的操作可见。

-
synchronized 和 ReentrantLock 都拥有可重入特点。可重入指线程已获得锁后再次请求同一把锁,无需先释放即可继续使用。
ReentrantLock是Lock接口最主要的实现类。
不同点
主要讲 7 点大的不同。
-
用法区别:synchronized 可加在方法上(锁对象为 this,无需指定),也可新建同步代码块并自定义 monitor 锁对象;Lock 接口必须显式用锁对象
lock()加锁、unlock()解锁,一般放在 finally 中以防死锁。synchronized 加解锁是隐式的,抛异常时也能保证释放锁。 -
加解锁顺序不同:Lock 可不完全按加锁反序解锁,有一定灵活度。
javalock1.lock(); lock2.lock(); ... lock1.unlock(); lock2.unlock();synchronized 解锁顺序与加锁顺序必须完全相反:
javasynchronized(obj1){ synchronized(obj2){ ... } }顺序为先 obj1 加锁、再 obj2 加锁、再 obj2 解锁、最后 obj1 解锁。synchronized 加解锁由 JVM 实现,执行完 synchronized 块自动解锁,按嵌套顺序加解锁,不能自行控制。
-
synchronized 锁不够灵活:锁被某个线程获得后,其他线程只能阻塞直到持有线程运行完毕或异常释放。持有线程长时间持有会降低程序效率,永不释放则等待线程永远等下去。Lock 用
lockInterruptibly可在等待过久时中断退出,也可用tryLock()尝试获取锁,获取不到可做别的事,更灵活。 -
synchronized 锁只能同时被一个线程拥有,Lock 无此限制:如读写锁中的读锁可被多个线程同时持有,synchronized 做不到。
-
原理区别:synchronized 是内置锁,由 JVM 实现获取/释放锁,分偏向锁、轻量级锁、重量级锁。Lock 按实现不同原理不同,如
ReentrantLock内部通过 AQS 获取和释放锁。 -
是否可设置公平/非公平:公平锁按先来后到原则依次获得锁。
ReentrantLock等 Lock 实现类可设置公平或非公平,synchronized 不能设置。 -
性能区别:Java 5 及之前 synchronized 性能较低;Java 6 后 JDK 对 synchronized 做了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等优化,后期版本中 synchronized 性能不比 Lock 差。
如何选择
《Java并发编程实战》和《Java核心技术》的结论:
- 若能不用,最好既不使用 Lock 也不使用 synchronized。多数情况可用
java.util.concurrent包中的机制,它会处理所有加解锁操作,优先推荐使用工具类。 - 若 synchronized 适合程序,尽量使用它,可减少代码量和出错概率。忘记在 finally 里 unlock 会引发大问题,synchronized 更安全。
- 若特别需要 Lock 的特殊功能(尝试获取锁、可中断、超时功能等),才使用 Lock。
Lock 常用方法
简介
Lock 接口是 Java 5 引入的,最常见的实现类是 ReentrantLock。Lock 与 synchronized 是两种最常见的锁。锁是控制共享资源访问的工具,二者都能保证线程安全,但在使用和功能上差异较大。Lock 并非代替 synchronized,而是当 synchronized 不合适或不足以满足要求时提供更高级功能。
通常 Lock 只允许一个线程访问共享资源。特殊实现可允许并发访问,如 ReadWriteLock 中的 ReadLock。
方法纵览
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
与加解锁相关的 5 个方法:lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()、unlock()。
lock() 方法
lock() 是最基础的获取锁方法。线程获取锁时若锁已被其他线程获取则等待。
Lock 的加锁与释放锁都是显式的(不像 synchronized 隐式、异常时自动释放),必须写代码释放。使用 lock() 时必须主动释放锁,最佳实践是 lock() 后先用 try{} 操作同步资源,必要时 catch{} 捕获异常,最后 finally{} 释放锁:
Lock lock = ...;
lock.lock();
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
}finally{
lock.unlock(); //释放锁
}
忘记在 finally 释放锁会让 Lock 非常危险:异常可能跳过 unlock(),导致锁永远无法释放、其他线程无法获得,这是 Lock 相对 synchronized 的劣势。
lock() 方法不能被中断,一旦死锁会陷入永久等待,一般用 tryLock() 等更高级方法代替。
tryLock()
tryLock() 尝试获取锁,未被其他线程占用则获取成功返回 true,否则返回 false。该方法立即返回,拿不到锁也不等待,可用 if 判断返回结果执行不同逻辑:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则做其他事情
}
可用 tryLock() 解决死锁问题:
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
System.out.println("获取到了两把锁,完成业务逻辑");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
} else {
Thread.sleep(new Random().nextInt(1000));
}
}
}
若不用 tryLock() 可能产生死锁:两个线程同时调用该方法且传入的 lock1、lock2 相反时,各自持有一把锁后互相尝试获取对方持有的锁而陷入死锁。tryLock() 先检测能否获取 lock1,能则再尝试 lock2;lock1 获取不到则随机时间等待,让其他线程释放锁;获取到 lock1 但获取不到 lock2 时释放 lock1 再随机等待。只有同时获取两把锁才执行逻辑并返回。
tryLock(long time, TimeUnit unit)
tryLock() 的重载方法,带超时时间。拿不到锁时等待指定时间,超时后仍获取不到则返回 false;一开始或等待期间获取到锁则返回 true。
该方法解决 lock() 易死锁的问题:等待指定超时时间后主动放弃获取,避免永久等待;等待期间可随时中断线程,避免死锁。与 lockInterruptibly() 类似。
lockInterruptibly()
获取锁,若锁当前可获得则立即返回;若被其他线程持有则开始等待,直到等到锁或被中断。即除非在获取锁期间被中断,否则一直尝试获取直到获取到。
lockInterruptibly() 可响应中断(synchronized 不可),程序更灵活。可理解为超时时间为无穷长的 tryLock(long time, TimeUnit unit)(二者都能响应中断,但 lockInterruptibly() 永不超时)。
该方法会抛出 InterruptedException,不在方法签名声明时需写两个 try 块:
public void lockInterruptibly() {
try {
lock.lockInterruptibly();
try {
System.out.println("操作资源");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
获取到锁后同样须用 try finally 保障锁的绝对释放。
unlock()
用于解锁。对 ReentrantLock,执行 unlock() 时把锁的"被持有计数器"减 1,减到 0 代表锁完全释放。减 1 后计数器不为 0 说明锁之前被"重入",锁未真正释放,仅减少持有次数。
公平锁与非公平锁
什么是公平和非公平
公平锁按线程请求的顺序分配锁;非公平锁不完全按请求顺序,在一定情况下允许插队。非公平并非完全随机、任意插队,而是仅在"合适的时机"插队。
合适的时机:当前线程请求获取锁时,恰巧前一个持有锁的线程释放了这把锁,当前线程可不顾已等待的线程立刻插队。若请求时前一个线程未在那一时刻释放锁,当前线程仍进入等待队列。
为什么要设置非公平策略
非公平是 ReentrantLock 的默认策略。与公平锁需唤醒等待线程的高开销有关:
- 线程 A 持有锁,线程 B 请求时被挂起进入阻塞。
- 线程 A 释放锁时本该轮到 B 苏醒,若线程 C 突然插队请求,非公平策略会把锁给 C。
- 唤醒 B 开销大,很可能唤醒前 C 已拿到锁并执行完任务、释放锁。插队让 C 跳过阻塞过程,锁内代码执行内容不多时 C 很快完成,在 B 完全唤醒前就把锁交出,形成双赢:C 无需等待提高效率,B 获得锁的时间未推迟(被唤醒时 C 早已释放锁)。
Java 设计非公平锁是为了提高整体运行效率。
公平的场景
创建公平锁,4 个线程按顺序请求。线程 1 拿到锁后,线程 2、3、4 在等待队列等待;线程 1 释放锁后,线程 2、3、4 依次获取,线程 2 因等待时间最长先获取。


不公平的场景
线程 1 解锁时线程 5 突然尝试获取锁,非公平策略下线程 5 可拿到锁,尽管未进入等待队列且线程 2、3、4 等待时间更长。从整体效率考虑,锁此时交给线程 5 持有。

代码案例:演示公平和非公平的效果
/**
* 描述:演示公平锁,分别展示公平和不公平的情况,非公平锁会让现在持有锁的线程优先再次获取到锁。代码借鉴自Java并发编程实战手册2.7。
*/
public class FairAndUnfair {
public static void main(String args[]) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue), "Thread " + i);
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
private PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
}
}
class PrintQueue {
private final Lock queueLock = new ReentrantLock(false);
public void printJob(Object document) {
queueLock.lock();
try {
Long duration = (long) (Math.random() * 10000);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
Thread.currentThread().getName(), (duration / 1000));
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
Long duration = (long) (Math.random() * 10000);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
Thread.currentThread().getName(), (duration / 1000));
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
通过改变 new ReentrantLock(false) 的参数设置公平/非公平锁。
公平情况下的输出:
Thread 0: Going to print a job
Thread 0: PrintQueue: Printing a Job during 5 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 1: PrintQueue: Printing a Job during 3 seconds
Thread 2: PrintQueue: Printing a Job during 4 seconds
Thread 3: PrintQueue: Printing a Job during 3 seconds
Thread 4: PrintQueue: Printing a Job during 9 seconds
Thread 5: PrintQueue: Printing a Job during 5 seconds
Thread 6: PrintQueue: Printing a Job during 7 seconds
Thread 7: PrintQueue: Printing a Job during 3 seconds
Thread 8: PrintQueue: Printing a Job during 9 seconds
Thread 9: PrintQueue: Printing a Job during 5 seconds
Thread 0: PrintQueue: Printing a Job during 8 seconds
Thread 0: The document has been printed
Thread 1: PrintQueue: Printing a Job during 1 seconds
Thread 1: The document has been printed
Thread 2: PrintQueue: Printing a Job during 8 seconds
Thread 2: The document has been printed
Thread 3: PrintQueue: Printing a Job during 2 seconds
Thread 3: The document has been printed
Thread 4: PrintQueue: Printing a Job during 0 seconds
Thread 4: The document has been printed
Thread 5: PrintQueue: Printing a Job during 7 seconds
Thread 5: The document has been printed
Thread 6: PrintQueue: Printing a Job during 3 seconds
Thread 6: The document has been printed
Thread 7: PrintQueue: Printing a Job during 9 seconds
Thread 7: The document has been printed
Thread 8: PrintQueue: Printing a Job during 5 seconds
Thread 8: The document has been printed
Thread 9: PrintQueue: Printing a Job during 9 seconds
Thread 9: The document has been printed
线程获取锁的顺序完全公平,先到先得。
非公平情况下的输出:
Thread 0: Going to print a job
Thread 0: PrintQueue: Printing a Job during 6 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 0: PrintQueue: Printing a Job during 8 seconds
Thread 0: The document has been printed
Thread 1: PrintQueue: Printing a Job during 9 seconds
Thread 1: PrintQueue: Printing a Job during 8 seconds
Thread 1: The document has been printed
Thread 2: PrintQueue: Printing a Job during 6 seconds
Thread 2: PrintQueue: Printing a Job during 4 seconds
Thread 2: The document has been printed
Thread 3: PrintQueue: Printing a Job during 9 seconds
Thread 3: PrintQueue: Printing a Job during 8 seconds
Thread 3: The document has been printed
Thread 4: PrintQueue: Printing a Job during 4 seconds
Thread 4: PrintQueue: Printing a Job during 2 seconds
Thread 4: The document has been printed
Thread 5: PrintQueue: Printing a Job during 2 seconds
Thread 5: PrintQueue: Printing a Job during 5 seconds
Thread 5: The document has been printed
Thread 6: PrintQueue: Printing a Job during 2 seconds
Thread 6: PrintQueue: Printing a Job during 6 seconds
Thread 6: The document has been printed
Thread 7: PrintQueue: Printing a Job during 6 seconds
Thread 7: PrintQueue: Printing a Job during 4 seconds
Thread 7: The document has been printed
Thread 8: PrintQueue: Printing a Job during 3 seconds
Thread 8: PrintQueue: Printing a Job during 6 seconds
Thread 8: The document has been printed
Thread 9: PrintQueue: Printing a Job during 3 seconds
Thread 9: PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed
非公平情况下存在抢锁"插队"现象,如 Thread 0 释放锁后又优先获取到锁,尽管等待队列已有 Thread 1 ~ Thread 9 排队。
对比公平和非公平的优缺点

- 公平锁:优点在于线程公平平等,每个线程等待一段时间后都有执行机会;缺点是整体执行速度更慢、吞吐量更小。
- 非公平锁:优势是整体执行速度更快、吞吐量更大;但可能产生线程饥饿问题,若有线程一直插队,等待队列中的线程可能长时间得不到运行。
源码分析
ReentrantLock 包含一个 Sync 类,继承自 AQS(AbstractQueuedSynchronizer):
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
Sync 类:
abstract static class Sync extends AbstractQueuedSynchronizer {...}
Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类:
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
公平锁的锁获取源码:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
非公平锁的锁获取源码:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { //这里没有判断 hasQueuedPredecessors()
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁与非公平锁的 lock() 方法唯一区别在于公平锁多了一个限制条件:hasQueuedPredecessors() 为 false,即判断等待队列中是否已有线程排队。公平锁一旦有线程排队,当前线程不再尝试获取锁;非公平锁无论是否已有线程排队都尝试获取,获取不到再排队。
特例:tryLock() 方法不遵守设定的公平原则。有线程执行 tryLock() 时,一旦有线程释放锁,正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式、即使它之前已有其他线程在等待队列等待,即 tryLock 可以插队。
源码:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
这里调用 nonfairTryAcquire(),表明不公平,与锁本身是否为公平锁无关。
综上:公平锁按线程申请顺序获取锁,实现公平;非公平锁加锁时不考虑排队情况直接尝试获取,存在后申请却先获得锁的情况,但提高了整体效率。
读写锁 ReadWriteLock 的规则
没有读写锁时使用普通 ReentrantLock,虽保证线程安全但浪费资源:多个读操作同时进行并无线程安全问题,可并行以提高效率。
写操作不是线程安全的,多个线程同时写,或写的同时进行读,都会造成线程安全问题。
读写锁设定一套规则,既保证多个线程同时读的效率,又保证有写入操作时的线程安全。
整体思路是两把锁:
- 写锁:获得后可读数据也可修改数据。
- 读锁:获得后只能查看数据,不能修改。读锁可被多个线程同时持有,多个线程可同时查看数据。
在读的地方用读锁、写的地方用写锁,灵活控制可提高执行效率。
读写锁的获取规则
- 一个线程已占读锁,其他线程申请读锁可以成功(读读共享)。
- 一个线程已占读锁,其他线程申请写锁不能成功(读写互斥)。
- 一个线程已占写锁,其他线程申请读锁或写锁都不能成功(写写互斥、写读互斥)。
总结:要么是一个或多个线程同时持有读锁,要么是一个线程持有写锁,两者不会同时出现,即读读共享、其他都互斥。
使用案例
ReentrantReadWriteLock 是 ReadWriteLock 的实现类,主要有两个方法 readLock() 和 writeLock() 获取读锁和写锁。
/**
* 描述: 演示读写锁用法
*/
public class ReadWriteLockDemo {
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> read()).start();
new Thread(() -> read()).start();
new Thread(() -> write()).start();
new Thread(() -> write()).start();
}
}
运行结果:
Thread-0得到读锁,正在读取
Thread-1得到读锁,正在读取
Thread-0释放读锁
Thread-1释放读锁
Thread-2得到写锁,正在写入
Thread-2释放写锁
Thread-3得到写锁,正在写入
Thread-3释放写锁
读锁可同时被多个线程获得,写锁不能。
读写锁适用场合
ReentrantLock 适用于一般场合;ReadWriteLock 适用于读多写少的情况,合理使用可进一步提高并发效率。
读写锁的插队与升降级
读锁插队策略
回顾第 24 节:ReentrantLock 设为非公平时,可在前面线程释放锁的瞬间插队而无需排队。读写锁策略不同。
ReentrantReadWriteLock 可设为公平或非公平:
公平锁:
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
非公平锁:
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
公平锁传入 true,非公平锁传入 false,默认为非公平锁。获取读锁前检查 readerShouldBlock() 方法,获取写锁前检查 writerShouldBlock() 方法,决定插队还是排队。
公平锁对两个方法的实现:
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
公平锁下,只要等待队列中有线程等待(hasQueuedPredecessors() 返回 true),writer 和 reader 都 block,一律不允许插队、都需排队,符合公平锁思想。
非公平锁的实现:
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
writerShouldBlock() 始终返回 false,写锁线程随时可插队(与 ReentrantLock 设计一致)。读锁则不同。
场景:线程 2 和线程 4 同时读取,线程 3 想写入但因已有线程持有读锁进入等待队列。此时线程 5 突然插队获取读锁。

有两种应对策略。
第一种策略:允许插队
让线程 5 直接加入线程 2 和线程 4 一起读取,不会特别增加读的负担(线程可共用锁)。
问题:若想读取的线程不断增加(如线程 6 也可插队),读锁长时间不被释放,导致需要写锁的线程 3 陷入"饥饿",长时间得不到执行。

第二种策略:不允许插队
虽然线程 5 直接插队可提高效率,但因线程 3 已提前等待,仍让线程 5 排队等待。

线程 5 放入等待队列,排在线程 3 后面,让线程 3 优先执行,避免"饥饿",对程序健壮性有好处。直到线程 3 运行完毕线程 5 才有机会运行,谁都不会等待太久。

结论:即便非公平锁,只要等待队列头结点是尝试获取写锁的线程,读锁仍不能插队,目的是避免"饥饿"。
策略选择演示
策略选择取决于具体锁实现,ReentrantReadWriteLock 选择策略 2。
代码演示:
/**
* 描述: 演示读锁不插队
*/
public class ReadLockJumpQueue {
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> read(),"Thread-2").start();
new Thread(() -> read(),"Thread-4").start();
new Thread(() -> write(),"Thread-3").start();
new Thread(() -> read(),"Thread-5").start();
}
}
运行结果:
Thread-2得到读锁,正在读取
Thread-4得到读锁,正在读取
Thread-2释放读锁
Thread-4释放读锁
Thread-3得到写锁,正在写入
Thread-3释放写锁
Thread-5得到读锁,正在读取
Thread-5释放读锁
结果说明 ReentrantReadWriteLock 选择"不允许插队"策略,大大减小"饥饿"概率。(运行结果不一致时,可在每个线程启动后增加 100ms 睡眠保证运行顺序。)
锁的升降级
读写锁降级功能代码演示
以下代码演示更新缓存时如何利用锁的降级功能:
public class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
//在获取写锁之前,必须首先释放读锁。
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
//这里需要再次判断数据的有效性,因为在我们释放读锁和获取写锁的空隙之内,可能有其他线程修改了数据。
if (!cacheValid) {
data = new Object();
cacheValid = true;
}
//在不释放写锁的情况下,直接获取读锁,这就是读写锁的降级。
rwl.readLock().lock();
} finally {
//释放了写锁,但是依然持有读锁
rwl.writeLock().unlock();
}
}
try {
System.out.println(data);
} finally {
//释放读锁
rwl.readLock().unlock();
}
}
}
processCachedData 先获取读锁 rwl.readLock().lock() 判断缓存是否有效:有效则跳过整个 if;失效则需更新缓存,需要获取写锁。
获取写锁前先释放读锁,再用 rwl.writeLock().lock() 获取写锁,用 try-finally。try 中先判断缓存是否有效(释放读锁和获取写锁间隙可能有其他线程修改数据,需二次判断)。缓存无效则用 new Object() 获取新数据并设 cacheValid 为 true。因后续要打印 data 值,不能在此释放所有锁,选择在不释放写锁的情况下直接获取读锁 rwl.readLock().lock(),然后在持有读锁的情况下释放写锁,最后在 try 中打印 data。
这是典型的利用锁降级功能的代码。
为什么需要锁的降级?
若一直使用写锁、最后才释放,虽线程安全但没必要,因为只有一处修改数据的代码:
data = new Object();
后续对 data 仅是读取。一直用写锁不能让多个线程同时读取,浪费资源、降低效率。锁的降级可提高整体性能。
支持锁的降级,不支持升级
不释放读锁直接尝试获取写锁(锁的升级)会让线程直接阻塞,程序无法运行:
final static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
upgrade();
}
public static void upgrade() {
rwl.readLock().lock();
System.out.println("获取到了读锁");
rwl.writeLock().lock();
System.out.println("成功升级");
}
代码打印"获取到了读锁"但不会打印"成功升级",因为 ReentrantReadWriteLock 不支持读锁升级到写锁。
为什么不支持锁的升级?
读写锁特点:多个线程可同时持有读锁,写锁只能一个线程持有,且不存在读锁和写锁同时持有的情况。
正因为不可能读锁和写锁同时持有,升级写锁需等到所有读锁都释放才能进行。设 A、B、C 三线程都持有读锁,线程 A 尝试从读锁升级到写锁,必须等待 B、C 释放读锁。若 B、C 逐渐释放,A 可成功升级获取写锁。
特殊死锁情况:设线程 A 和 B 都想升级到写锁。对 A 而言需等待包括 B 在内所有线程释放读锁,对 B 也需等待包括 A 释放读锁,形成典型死锁,谁都不愿率先释放手中的锁。
读写锁升级并非不可能,若保证每次只有一个线程可升级,即可保证线程安全。只是最常见的 ReentrantReadWriteLock 不支持。
总结
对 ReentrantReadWriteLock:
- 插队策略
- 公平策略下,只要队列里有线程排队就不允许插队。
- 非公平策略下:
- 若允许读锁插队,读锁可被多线程同时持有,可能源源不断的后续线程插队成功,导致读锁一直不能完全释放、写锁一直等待。为防止"饥饿",当等待队列头结点是尝试获取写锁的线程时,不允许读锁插队。
- 写锁可随时插队。写锁不易插队成功(只有当前没有任何线程持有读锁和写锁时才能插队成功),插队失败即进入等待队列,很难造成"饥饿",允许写锁插队是为提高效率。
- 升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁。
自旋锁
什么是自旋
"自旋"即"自我旋转","旋转"指"循环"(如 while/for 循环)。"自旋"是自己不停地循环直到目标达成,不像普通锁获取不到锁就进入阻塞。
对比自旋和非自旋的获取锁的流程

- 自旋锁:不放弃 CPU 时间片,通过自旋等待锁释放,不停再次尝试获取锁,失败再试直到成功。
- 非自旋锁:获取不到锁时切换线程状态让线程休眠,CPU 去做其他事;持有锁的线程释放锁后,CPU 恢复该线程再尝试获取。再失败则再次休眠,成功则获取到同步资源锁。
两者最大区别:非自旋锁遇到拿不到锁会把线程阻塞直到被唤醒;自旋锁会不停尝试。
自旋锁的好处
阻塞和唤醒线程开销高昂。若同步代码块内容不复杂,线程切换开销可能比实际业务代码执行开销还大。
同步代码块内容不多、执行时间短时,与其切换线程状态,不如让线程自旋尝试获取锁,等待其他线程释放锁。有时稍等即可避免上下文切换等开销,提高效率。
总结:自旋锁用循环不停尝试获取锁,让线程始终处于 Runnable 状态,节省线程状态切换开销。
AtomicLong 的实现
Java 1.5 及以上的 java.util.concurrent 包中原子类基本都是自旋锁实现。
AtomicLong 的 getAndIncrement 方法源码:
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
其调用 unsafe.getAndAddLong:
public final long getAndAddLong (Object var1,long var2, long var4){
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
其中用 do-while 循环:
do {
var6 = this.getLongVolatile(var1, var2);
}
while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
do-while 循环即自旋操作,修改过程中遇到其他线程竞争导致修改失败时,在 while 循环中死循环直到修改成功。
自己实现一个可重入的自旋锁
package lesson27;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
/**
* 描述: 实现一个可重入的自旋锁
*/
public class ReentrantSpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
//重入次数
private int count = 0;
public void lock() {
Thread t = Thread.currentThread();
if (t == owner.get()) {
++count;
return;
}
//自旋获取锁
while (!owner.compareAndSet(null, t)) {
System.out.println("自旋了");
}
}
public void unlock() {
Thread t = Thread.currentThread();
//只有持有锁的线程才能解锁
if (t == owner.get()) {
if (count > 0) {
--count;
} else {
//此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁
owner.set(null);
}
}
}
public static void main(String[] args) {
ReentrantSpinLock spinLock = new ReentrantSpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
运行结果:
...
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
自旋了
Thread-0释放了了自旋锁
Thread-1获取到了自旋锁
前面打印很多"自旋了",说明自旋期间 CPU 依然不停运转。
缺点
自旋锁最大的缺点是:避免线程切换开销的同时带来新开销,需要不停尝试获取锁。若锁一直不能释放,这些尝试是无效尝试,白白浪费处理器资源。虽然一开始自旋锁开销低于线程切换,但随时间的增加开销水涨船高,后期甚至会超过线程切换开销,得不偿失。
适用场景
自旋锁适用于并发度不是特别高、临界区较短小的场景,可利用避免线程切换提高效率。
若临界区很大,线程拿到锁很久才释放,不适合用自旋锁:自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。
本文流程图参考自 https://tech.meituan.com/2018/11/15/java-lock.html 自旋锁的实现的代码来自 https://www.fatalerrors.org/a/java-implementation-of-spin-lock.html
JVM 的锁优化
JDK 1.6 中 HotSpot 虚拟机对 synchronized 内置锁进行了多项优化,包括自适应的自旋、锁消除、锁粗化、偏向锁、轻量级锁等,大幅提高了 synchronized 锁的性能。
自适应的自旋锁
自旋指不释放 CPU,一直循环尝试获取锁,如:
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
自旋的缺点是:自旋时间过长时性能开销大,浪费 CPU 资源。
JDK 1.6 引入自适应的自旋锁解决长时间自旋问题。自适应指自旋时间不固定,而是根据最近自旋尝试的成功率、失败率、当前锁拥有者的状态等因素共同决定。最近自旋获取某把锁成功,则下次可能继续自旋并允许更长时间;最近自旋失败,则可能省略自旋过程,减少无用的自旋,提高效率。
锁消除
经过逃逸分析后,若某些对象不可能被其他线程访问到,则可视为栈上数据。栈上数据仅本线程可访问,天然线程安全,无需加锁,JVM 会自动去除这类锁。
例如 StringBuffer 的 append 方法:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
该方法被 synchronized 修饰,但多数情况下仅在一个线程内使用。若编译器能确定该 StringBuffer 对象只在单线程内使用,则消除对应的 synchronized,省去加锁和解锁操作,提升效率。
锁粗化
当释放锁后紧接着又重新获取锁(未做其他操作)时:
public void lockCoarsening() {
synchronized (this) {
//do something
}
synchronized (this) {
//do something
}
synchronized (this) {
//do something
}
}
这种释放与重新获取是无意义的。扩大同步区域,只在最开始加一次锁、最后直接解锁,可消除中间无意义的加解锁操作,将多个 synchronized 块合并为一个较大的同步块,减少频繁申请与释放锁的开销。
副作用是同步区域变大。在循环中做同样操作时:
for (int i = 0; i < 1000; i++) {
synchronized (this) {
//do something
}
}
若从第一次循环开始扩大同步区域并持有锁,直到最后一次循环结束才释放,会导致其他线程长时间无法获得锁。因此锁粗化不适用于循环场景,仅适用于非循环场景。
锁粗化功能默认打开,用 -XX:-EliminateLocks 关闭。
偏向锁/轻量级锁/重量级锁
这三种锁是 synchronized 锁的状态,通过对象头中的 mark word 表明锁状态。
- 偏向锁:若自始至终不存在锁竞争,则无需上锁,只需打标记。对象初始化后没有任何线程获取锁时为可偏向状态,第一个线程访问时记录该线程;后续尝试获取锁的线程正是偏向锁拥有者时可直接获取锁,开销很小。
版本更新(JEP 374):JDK 15 起偏向锁默认禁用,
-XX:+UseBiasedLocking参数已废弃;JDK 21 中偏向锁不再参与默认锁升级路径。下列轻量级/重量级锁机制仍然有效,偏向锁仅作历史机制理解。
-
轻量级锁:当锁原来是偏向锁时被另一线程访问,说明存在竞争,偏向锁升级为轻量级锁。线程通过自旋方式尝试获取锁,不会阻塞。轻量级锁适用场景是 synchronized 代码块被多个线程交替执行、无实际竞争或竞争时间短,用 CAS 即可解决。
-
重量级锁:利用操作系统的同步机制实现,开销较大。当多个线程存在实际竞争且竞争时间较长时,偏向锁和轻量级锁无法满足需求,锁膨胀为重量级锁,会让申请不到锁的线程进入阻塞状态。
锁升级的路径
锁升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(JDK 15 前)。
- 偏向锁:性能最好,避免 CAS 操作。
- 轻量级锁:利用自旋和 CAS,避免重量级锁带来的线程阻塞和唤醒,性能中等。
- 重量级锁:阻塞获取不到锁的线程,性能最差。
JVM 默认优先使用偏向锁,有必要时逐步升级,大幅提高锁性能。
版本更正:该结论仅适用于 JDK 15 之前。JEP 374 自 JDK 15 起默认禁用偏向锁(无锁直接进入轻量级锁路径),JDK 21 同样如此。现代应用无需也无法通过参数重新启用偏向锁。
版本差异(旧版 → Java 21)
| 特性 | 旧版(JDK 8/11) | JDK 15/21 |
|---|---|---|
| 偏向锁 | 默认启用,回收成本高 | 默认禁用(JEP 374),-XX:+UseBiasedLocking 已废弃 |
| 锁升级路径 | 无锁→偏向→轻量→重量 | 无锁→轻量→重量(偏向锁不再参与) |
| synchronized | 性能接近 Lock | 不变;虚拟线程(21)让阻塞式锁更廉价 |
| 虚拟线程同步 | 无 | synchronized 在虚拟线程中可阻塞挂载线程(JDK 21) |