【JVM】synchronized锁优化与锁升级全解析
JDK 6之前,synchronized是重量级锁,性能较差。JDK 6引入了偏向锁、轻量级锁等优化机制,大幅提升了synchronized的性能。本文深入剖析JVM对synchronized的各种优化策略和锁升级的完整过程。
为什么需要锁优化
重量级锁的问题
JDK 6之前,synchronized直接使用操作系统的互斥量(Mutex Lock)实现:
1 2 3 4 5
| 线程获取锁失败 → 阻塞 → 操作系统介入 → 用户态切换到内核态
用户态 ←──────────────────────────────▶ 内核态 上下文切换 (数千个CPU周期)
|
问题:
- 每次加锁/解锁都需要系统调用
- 用户态与内核态切换开销巨大
- 即使没有竞争也要付出这个代价
实际场景分析
研究发现,大多数情况下:
- 大部分锁不存在竞争
- 很多锁总是由同一个线程获取
- 竞争往往很短暂
针对这些特点,JVM引入了锁升级机制。
对象头与Mark Word
Java对象内存布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ┌─────────────────────────────────────────────────────────┐ │ Java对象 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 对象头(Header) │ │ ├── Mark Word (8字节,64位JVM) │ │ ├── Class Pointer (4/8字节,类型指针) │ │ └── 数组长度 (4字节,仅数组对象) │ │ │ │ 实例数据(Instance Data) │ │ └── 对象的字段数据 │ │ │ │ 对齐填充(Padding) │ │ └── 8字节对齐 │ │ │ └─────────────────────────────────────────────────────────┘
|
Mark Word详解
Mark Word是实现锁的关键,它的结构会随锁状态变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 64位JVM的Mark Word结构:
┌────────────────────────────────────────────────────────────────────┐ │ Mark Word (64 bits) │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ 无锁状态(Normal): │ │ ┌─────────────┬──────────────┬─────────┬────────┬───────┬───────┐│ │ │ unused │ hashcode │ unused │ age │biased │ lock ││ │ │ 25 bits │ 31 bits │ 1 bit │ 4 bits │ 1 bit │ 2 bits││ │ │ │ 对象哈希码 │ │ GC年龄 │ 0 │ 01 ││ │ └─────────────┴──────────────┴─────────┴────────┴───────┴───────┘│ │ │ │ 偏向锁状态(Biased): │ │ ┌────────────────────────────┬─────────┬────────┬───────┬───────┐│ │ │ thread ID │ epoch │ age │biased │ lock ││ │ │ 54 bits │ 2 bits │ 4 bits │ 1 bit │ 2 bits││ │ │ 持有锁的线程ID │ 偏向时间│ GC年龄 │ 1 │ 01 ││ │ └────────────────────────────┴─────────┴────────┴───────┴───────┘│ │ │ │ 轻量级锁状态(Lightweight Locked): │ │ ┌──────────────────────────────────────────────────────────┬─────┐│ │ │ ptr_to_lock_record │lock ││ │ │ 62 bits │2bits││ │ │ 指向栈中Lock Record的指针 │ 00 ││ │ └──────────────────────────────────────────────────────────┴─────┘│ │ │ │ 重量级锁状态(Heavyweight Locked): │ │ ┌──────────────────────────────────────────────────────────┬─────┐│ │ │ ptr_to_heavyweight_monitor │lock ││ │ │ 62 bits │2bits││ │ │ 指向Monitor对象的指针 │ 10 ││ │ └──────────────────────────────────────────────────────────┴─────┘│ │ │ │ GC标记状态: │ │ ┌──────────────────────────────────────────────────────────┬─────┐│ │ │ (空) │lock ││ │ │ │ 11 ││ │ └──────────────────────────────────────────────────────────┴─────┘│ │ │ └────────────────────────────────────────────────────────────────────┘
锁状态标志位: biased_lock | lock | 状态 ────────────────────────── 0 | 01 | 无锁 1 | 01 | 偏向锁 - | 00 | 轻量级锁 - | 10 | 重量级锁 - | 11 | GC标记
|
锁升级全过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ┌─────────────────────────────────────────────────────────────────────┐ │ 锁升级过程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 无锁 │ │ │ │ │ │ 第一个线程访问 │ │ ▼ │ │ 偏向锁 ─────────────────────────────────────────┐ │ │ │ │ │ │ │ 出现第二个线程竞争 │ │ │ ▼ │ │ │ 轻量级锁 ─────────────────────────┐ │ │ │ │ │ │ │ │ │ 自旋失败/竞争激烈 │ │ │ │ ▼ │ │ │ │ 重量级锁 │ │ │ │ │ │ │ │ │ │ │ │ 注意:锁只能升级,不能降级 │ │ │ │ (偏向锁批量重偏向除外) │ │ │ │ │ │ │ └────────────────────────────────────────┴──────────────┴─────────────┘
|
偏向锁(Biased Locking)
设计思想
统计表明,大多数情况下锁不仅不存在竞争,而且总是由同一个线程多次获得。偏向锁的目标是:让这个线程获取锁的代价尽可能低。
偏向锁原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 首次获取锁: ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ 线程A第一次进入同步块 │ │ │ │ │ ▼ │ │ 检查Mark Word的锁状态 │ │ │ │ │ ▼ │ │ biased=1, lock=01, threadID=0 (可偏向但未偏向) │ │ │ │ │ ▼ │ │ CAS将threadID设为当前线程ID │ │ │ │ │ ┌────┴────┐ │ │ 成功 失败 │ │ │ │ │ │ ▼ ▼ │ │ 获得偏向锁 说明有竞争, │ │ 升级为轻量级锁 │ │ │ └─────────────────────────────────────────────────────────────────────┘
再次获取锁(同一线程): ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ 线程A再次进入同步块 │ │ │ │ │ ▼ │ │ 检查Mark Word │ │ │ │ │ ▼ │ │ threadID == 当前线程ID ? │ │ │ │ │ 是 │ │ │ │ │ ▼ │ │ 直接进入同步块(无需任何同步操作!) │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
偏向锁的性能优势
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| synchronized (lock) { }
synchronized (lock) { }
synchronized (lock) { }
|
偏向锁的撤销
当其他线程尝试竞争偏向锁时,需要撤销偏向:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ┌─────────────────────────────────────────────────────────────────────┐ │ 偏向锁撤销过程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 线程B尝试获取被线程A偏向的锁 │ │ │ │ │ ▼ │ │ 等待全局安全点(Safe Point) │ │ (所有线程暂停,类似GC) │ │ │ │ │ ▼ │ │ 检查线程A的状态 │ │ │ │ │ ┌────┴────┐ │ │ 线程A已退出 线程A还在 │ │ 同步块 同步块中 │ │ │ │ │ │ ▼ ▼ │ │ 将锁对象设为 升级为轻量级锁 │ │ 无锁状态, 线程A持有轻量级锁 │ │ 然后线程B 线程B自旋等待 │ │ 重新竞争 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
偏向锁撤销的代价很高(需要STW),所以JVM有以下优化:
批量重偏向(Bulk Rebias)
1 2 3 4 5 6 7 8 9 10
| 如果一个类的对象被多个线程交替访问:
对象1 偏向线程A → 撤销 → 偏向线程B 对象2 偏向线程A → 撤销 → 偏向线程B 对象3 偏向线程A → 撤销 → 偏向线程B ...
当撤销次数达到阈值(默认20),JVM会: - 批量将该类所有对象重偏向到当前线程 - 更新类的epoch值
|
批量撤销(Bulk Revoke)
1 2 3 4
| 如果撤销次数继续增加(默认40),JVM会: - 禁用该类的偏向锁功能 - 将该类所有对象的偏向标记设为0 - 之后该类对象直接使用轻量级锁
|
偏向锁的JVM参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| -XX:+UseBiasedLocking
-XX:-UseBiasedLocking
-XX:BiasedLockingStartupDelay=0
-XX:BiasedLockingBulkRebiasThreshold=20
-XX:BiasedLockingBulkRevokeThreshold=40
|
注意:JDK 15开始,偏向锁默认关闭,JDK 18+已废弃偏向锁。
轻量级锁(Lightweight Locking)
设计思想
当存在轻微竞争时,使用CAS自旋代替操作系统互斥量,避免内核态切换。
轻量级锁原理
Lock Record(锁记录)
1 2 3 4 5 6 7 8 9 10 11
| 线程栈帧中的Lock Record结构:
┌─────────────────────────────────┐ │ Lock Record │ ├─────────────────────────────────┤ │ Displaced Mark Word │ ← 保存对象原来的Mark Word │ (用于恢复) │ ├─────────────────────────────────┤ │ Owner (ptr to object) │ ← 指向锁对象 │ │ └─────────────────────────────────┘
|
加锁过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| ┌─────────────────────────────────────────────────────────────────────┐ │ 轻量级锁加锁过程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 步骤1:在栈帧中创建Lock Record │ │ │ │ 线程栈 锁对象 │ │ ┌─────────────┐ ┌─────────────┐ │ │ │Lock Record │ │ Mark Word │ │ │ │┌───────────┐│ │ (无锁状态) │ │ │ ││Displaced ││ │ hashcode │ │ │ ││Mark Word ││ │ age | 0 |01 │ │ │ │└───────────┘│ └─────────────┘ │ │ │┌───────────┐│ │ │ ││ owner ───┼┼────────────────────▶ │ │ │└───────────┘│ │ │ └─────────────┘ │ │ │ │ 步骤2:复制Mark Word到Lock Record │ │ │ │ 线程栈 锁对象 │ │ ┌─────────────┐ ┌─────────────┐ │ │ │Lock Record │ │ Mark Word │ │ │ │┌───────────┐│ 复制 │ hashcode │ │ │ ││ hashcode ││◀───────────────│ age | 0 |01 │ │ │ ││ age|0|01 ││ └─────────────┘ │ │ │└───────────┘│ │ │ └─────────────┘ │ │ │ │ 步骤3:CAS替换Mark Word为指向Lock Record的指针 │ │ │ │ 线程栈 锁对象 │ │ ┌─────────────┐ ┌─────────────┐ │ │ │Lock Record │◀───────────────│ Mark Word │ │ │ │┌───────────┐│ CAS │ptr_to_LR|00 │ │ │ ││ hashcode ││ └─────────────┘ │ │ ││ age|0|01 ││ │ │ │└───────────┘│ │ │ └─────────────┘ │ │ │ │ CAS成功:获得轻量级锁,lock位变为00 │ │ CAS失败:存在竞争,进入自旋或升级 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
解锁过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ┌─────────────────────────────────────────────────────────────────────┐ │ 轻量级锁解锁过程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ CAS将Displaced Mark Word替换回对象头 │ │ │ │ 线程栈 锁对象 │ │ ┌─────────────┐ ┌─────────────┐ │ │ │Lock Record │ │ Mark Word │ │ │ │┌───────────┐│ CAS │ptr_to_LR|00 │ │ │ ││ hashcode ││───────────────▶│ │ │ │ ││ age|0|01 ││ (还原) │ hashcode │ │ │ │└───────────┘│ │ age | 0 |01 │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ CAS成功:解锁成功,恢复无锁状态 │ │ CAS失败:说明有其他线程在等待,锁已膨胀为重量级锁 │ │ 需要在释放锁的同时唤醒等待的线程 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
自旋优化
当CAS失败时,线程不会立即阻塞,而是自旋等待:
1 2 3 4 5 6 7 8 9 10 11
| int spinCount = 0; while (!cas_acquire_lock()) { spinCount++; if (spinCount > MAX_SPIN_COUNT) { inflate_to_heavyweight(); break; } }
|
自适应自旋(Adaptive Spinning)
JDK 6引入自适应自旋,JVM会根据历史数据动态调整自旋次数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ┌─────────────────────────────────────────────────────────────────────┐ │ 自适应自旋策略 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 如果上次在这个锁上自旋成功了: │ │ → 本次允许更长的自旋时间 │ │ → JVM认为这个锁很快会被释放 │ │ │ │ 如果上次在这个锁上自旋失败了: │ │ → 本次减少自旋时间,甚至跳过自旋 │ │ → JVM认为自旋可能是浪费CPU │ │ │ │ 如果锁的拥有者正在运行中: │ │ → 倾向于自旋(锁可能很快释放) │ │ │ │ 如果锁的拥有者已经阻塞了: │ │ → 不自旋,直接阻塞 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
重量级锁(Heavyweight Locking)
设计思想
当竞争激烈、自旋无效时,使用操作系统的互斥量实现真正的阻塞等待。
Monitor对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ┌─────────────────────────────────────────────────────────────────────┐ │ ObjectMonitor 结构 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ ObjectMonitor │ │ │ ├─────────────────────────────────────────────────────────────┤ │ │ │ _header : Mark Word (保存原来的对象头) │ │ │ │ _object : 指向锁对象 │ │ │ │ _owner : 当前持有锁的线程 │ │ │ │ _recursions : 重入次数 │ │ │ │ _count : 等待线程数 │ │ │ │ │ │ │ │ _EntryList : 阻塞等待获取锁的线程队列 │ │ │ │ ┌────┐ ┌────┐ ┌────┐ │ │ │ │ │ T1 │ │ T2 │ │ T3 │ 等待获取锁 │ │ │ │ └────┘ └────┘ └────┘ │ │ │ │ │ │ │ │ _WaitSet : 调用wait()后等待的线程队列 │ │ │ │ ┌────┐ ┌────┐ │ │ │ │ │ T4 │ │ T5 │ 等待被notify唤醒 │ │ │ │ └────┘ └────┘ │ │ │ │ │ │ │ │ _cxq : 最近到达的竞争者队列(ContentionQueue) │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
锁膨胀过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| ┌─────────────────────────────────────────────────────────────────────┐ │ 轻量级锁膨胀为重量级锁 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 线程A持有轻量级锁 │ │ │ │ │ ▼ │ │ 线程B尝试获取锁,CAS失败 │ │ │ │ │ ▼ │ │ 线程B自旋等待 │ │ │ │ │ ▼ │ │ 自旋次数超过阈值,仍未获取到锁 │ │ │ │ │ ▼ │ │ 触发锁膨胀(inflate) │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ 1. 分配ObjectMonitor对象 │ │ │ │ 2. 将对象原来的Mark Word保存到Monitor的_header字段 │ │ │ │ 3. 设置Monitor的_owner为当前锁的持有者(线程A) │ │ │ │ 4. 将对象的Mark Word替换为指向Monitor的指针 │ │ │ │ 5. 锁标志位设为10 │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 线程B进入EntryList阻塞等待 │ │ │ │ │ ▼ │ │ 线程A释放锁时,唤醒EntryList中的线程 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
重量级锁的工作流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| ┌─────────────────────────────────────────────────────────────────────┐ │ 重量级锁的获取与释放 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 获取锁: │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ if (_owner == NULL) { │ │ │ │ // 锁空闲,CAS获取 │ │ │ │ CAS(_owner, NULL, currentThread); │ │ │ │ } else if (_owner == currentThread) { │ │ │ │ // 重入 │ │ │ │ _recursions++; │ │ │ │ } else { │ │ │ │ // 竞争失败 │ │ │ │ 1. 先自旋尝试 │ │ │ │ 2. 自旋失败则加入_cxq队列 │ │ │ │ 3. 调用park()阻塞 │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ 释放锁: │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ if (_recursions > 0) { │ │ │ │ // 还在重入中 │ │ │ │ _recursions--; │ │ │ │ return; │ │ │ │ } │ │ │ │ _owner = NULL; │ │ │ │ // 唤醒等待线程 │ │ │ │ 从_EntryList或_cxq中取出一个线程 │ │ │ │ unpark(waitingThread); │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
其他锁优化
锁消除(Lock Elimination)
JIT编译器通过逃逸分析,消除不可能存在竞争的锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
public String concat(String s1, String s2) { StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append(s2); return sb.toString(); }
|
1 2 3 4 5 6 7 8
| -XX:+EliminateLocks
-XX:-EliminateLocks
-XX:+DoEscapeAnalysis
|
锁粗化(Lock Coarsening)
将多个连续的加锁解锁操作合并为一次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| for (int i = 0; i < 10000; i++) { synchronized (lock) { } synchronized (lock) { } }
synchronized (lock) { for (int i = 0; i < 10000; i++) { } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public void method() { synchronized (lock) { doSomething1(); } synchronized (lock) { doSomething2(); } synchronized (lock) { doSomething3(); } }
public void method() { synchronized (lock) { doSomething1(); doSomething2(); doSomething3(); } }
|
锁升级流程总结
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| ┌─────────────────────────────────────────────────────────────────────┐ │ 完整的锁升级流程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ │ │ │ 无锁 │ │ │ │ 001(01) │ │ │ └────┬────┘ │ │ │ │ │ 第一个线程访问 │ │ (延迟4秒后启用偏向) │ │ │ │ │ ▼ │ │ ┌─────────┐ │ │ │ 偏向锁 │ │ │ │ 101(01) │ │ │ └────┬────┘ │ │ │ │ │ ┌─────────┼─────────┐ │ │ │ │ │ │ │ 同一线程 其他线程竞争 批量撤销 │ │ 再次访问 (偏向锁撤销) 超过阈值 │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ 直接进入 ┌─────────┐ 禁用该类 │ │ (无CAS) │轻量级锁 │ 偏向锁 │ │ │ 00 │ │ │ └────┬────┘ │ │ │ │ │ ┌─────────┼─────────┐ │ │ │ │ │ │ │ CAS成功 CAS失败 自旋成功 │ │ │ (自旋) │ │ │ ▼ │ ▼ │ │ 获得锁 │ 获得锁 │ │ │ │ │ 自旋失败 │ │ (超过阈值) │ │ │ │ │ ▼ │ │ ┌─────────┐ │ │ │重量级锁 │ │ │ │ 10 │ │ │ └─────────┘ │ │ │ │ 说明:括号内为 biased_lock位 + lock位 │ │ │ └─────────────────────────────────────────────────────────────────────┘
|
各种锁的对比
| 锁类型 |
优点 |
缺点 |
适用场景 |
| 偏向锁 |
无竞争时几乎无开销 |
撤销需要STW |
单线程访问 |
| 轻量级锁 |
无阻塞,响应快 |
自旋消耗CPU |
竞争少,同步块执行快 |
| 重量级锁 |
不消耗CPU |
阻塞,上下文切换开销 |
竞争激烈 |
JVM调优参数汇总
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| -XX:+UseBiasedLocking -XX:-UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -XX:BiasedLockingBulkRebiasThreshold=20 -XX:BiasedLockingBulkRevokeThreshold=40
-XX:PreBlockSpin=10
-XX:+EliminateLocks -XX:+DoEscapeAnalysis
-XX:+PrintBiasedLockingStatistics -XX:+TraceBiasedLocking
|
总结
JVM对synchronized的优化是分层的:
- 偏向锁:针对无竞争场景,只需一次CAS,之后几乎无开销
- 轻量级锁:针对轻微竞争,通过CAS自旋避免阻塞
- 重量级锁:针对激烈竞争,使用操作系统互斥量
配合锁消除、锁粗化等优化,synchronized在大多数场景下性能已经很好,是Java并发编程的首选同步方式。