深入理解 synchronized
引言:synchronized的核心地位
在Java并发编程中,synchronized
关键字是实现线程安全的基石。自JDK 1.0引入以来,它经历了从"重量级锁"到"自适应锁"的进化,如今已成为兼顾安全性与性能的成熟方案。本文将从用法解析、字节码实现、底层原理、锁升级机制、JDK优化、性能对比到最佳实践,全方位剖析synchronized
的技术细节,结合OpenJDK源码与实测数据,带你彻底掌握这一并发利器。
一、synchronized的基本用法与语义
1.1 三种使用方式
synchronized
可修饰方法或代码块,核心是通过对象锁实现线程互斥。具体用法如下:
用法场景 | 锁对象 | 字节码实现 | 示例代码 |
---|---|---|---|
修饰实例方法 | 当前对象实例(this ) | 方法访问标志ACC_SYNCHRONIZED | public synchronized void increment() { count++; } |
修饰静态方法 | 类对象(Class 实例) | 方法访问标志ACC_SYNCHRONIZED | public static synchronized void staticIncrement() { staticCount++; } |
修饰代码块 | 显式指定对象 | monitorenter /monitorexit 指令 | synchronized (lockObj) { count++; } |
关键语义:
- 互斥性:同一时刻只有一个线程能持有锁,确保临界区代码串行执行。
- 可见性:释放锁时,线程会将工作内存中的修改刷新到主内存;获取锁时,线程会失效本地缓存,从主内存加载最新值(通过内存屏障实现)。
- 可重入性:线程可重复获取已持有的锁,通过
_recursions
计数器实现(见ObjectMonitor源码)。
1.2 用法示例与字节码分析
示例1:同步代码块
public class SyncBlockExample {private final Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) { // 显式指定lock为锁对象count++;}}
}
字节码反编译(javap -v SyncBlockExample.class
):
同步代码块通过monitorenter
(进入锁)和monitorexit
(释放锁)指令实现:
public void increment();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: getfield #2 // Field lock:Ljava/lang/Object;4: dup5: astore_1 // 将锁对象引用存入局部变量表6: monitorenter // 获取锁7: aload_08: dup9: getfield #3 // Field count:I12: iconst_113: iadd14: putfield #3 // Field count:I17: aload_118: monitorexit // 正常退出时释放锁19: goto 2722: astore_223: aload_124: monitorexit // 异常退出时释放锁25: aload_226: athrow27: return
注意:编译器会生成两个
monitorexit
,分别对应正常退出和异常退出,确保锁必定释放。
示例2:同步方法
public class SyncMethodExample {private int count = 0;public synchronized void increment() { // 实例方法锁,锁对象为thiscount++;}public static synchronized void staticIncrement() { // 静态方法锁,锁对象为SyncMethodExample.classstaticCount++;}
}
字节码特征:同步方法通过ACC_SYNCHRONIZED
标志实现,无需显式monitor
指令:
public synchronized void increment();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZED // 同步方法标志Code:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field count:I5: iconst_16: iadd7: putfield #2 // Field count:I10: return
二、底层实现:对象头与Monitor机制
2.1 对象头与Mark Word
synchronized
的实现依赖对象头(Object Header)中的Mark Word存储锁状态。对象头由两部分组成:
- Mark Word:存储对象运行时数据(哈希码、GC年龄、锁状态等)。
- Klass Pointer:指向类元数据的指针。
64位JVM Mark Word格式(不同锁状态下的存储结构):
锁状态 | 标志位 | 存储内容 |
---|---|---|
无锁 | 01 | 哈希码(25bit)+ GC年龄(4bit)+ 是否偏向锁(1bit=0)+ 标志位(2bit=01) |
偏向锁 | 01 | 偏向线程ID(54bit)+ Epoch(2bit)+ GC年龄(4bit)+ 是否偏向锁(1bit=1)+ 标志位(2bit=01) |
轻量级锁 | 00 | 指向栈中锁记录(Lock Record)的指针(64bit)+ 标志位(2bit=00) |
重量级锁 | 10 | 指向ObjectMonitor对象的指针(64bit)+ 标志位(2bit=10) |
GC标记 | 11 | 空 |
工具推荐:使用
org.openjdk.jol:jol-core
查看对象头,如ClassLayout.parseInstance(obj).toPrintable()
。
2.2 Monitor监视器锁
重量级锁的实现依赖ObjectMonitor(C++实现),每个对象关联一个Monitor,用于管理线程竞争与等待。
ObjectMonitor核心结构(OpenJDK源码objectMonitor.hpp
):
class ObjectMonitor {
private:void* volatile _owner; // 持有锁的线程ObjectWaiter* volatile _WaitSet; // 等待队列(调用wait()的线程)ObjectWaiter* volatile _EntryList; // 阻塞队列(未获取锁的线程)int _recursions; // 重入次数int _count; // 等待线程数// ...其他字段
};
工作流程:
- 竞争锁:线程通过
CAS
尝试将_owner
设为自身,成功则获取锁;失败则进入_EntryList
阻塞。 - 释放锁:线程退出同步块时,将
_owner
设为null
,唤醒_EntryList
中的线程重新竞争。 - 等待/唤醒:调用
wait()
时,线程释放锁并进入_WaitSet
;notify()
将线程从_WaitSet
移至_EntryList
重新竞争。
三、锁升级机制:从偏向锁到重量级锁
JDK 1.6引入锁升级机制,根据竞争程度动态选择锁状态(不可逆),平衡性能与安全性。
3.1 偏向锁(Biased Locking)
设计目标:减少单线程重复获取锁的开销。
实现原理:
- 首次获取锁时,通过
CAS
将线程ID记录到Mark Word,设为偏向模式(标志位101
)。 - 后续同一线程访问时,仅需比对线程ID,无需CAS操作。
撤销条件:当其他线程尝试竞争时,需等待全局安全点(STW),暂停持有线程,检查状态:
- 若持有线程已结束,重置为无锁状态。
- 若持有线程存活,升级为轻量级锁。
JVM参数:
-XX:+UseBiasedLocking
(默认启用,JDK 15后默认禁用)。-XX:BiasedLockingStartupDelay=0
(禁用启动延迟)。
3.2 轻量级锁(Lightweight Locking)
设计目标:应对多线程交替执行的轻度竞争。
实现步骤:
- 创建锁记录:线程在栈帧中创建
Lock Record
,复制Mark Word(Displaced Mark Word)。 - CAS竞争锁:通过
CAS
将Mark Word替换为指向Lock Record的指针(标志位00
)。 - 自旋重试:竞争失败时,线程自旋(空循环)尝试获取锁,避免阻塞(自适应自旋:根据历史成功率调整次数)。
升级条件:
- 自旋超过阈值(默认10次,JDK 1.7后自适应)。
- 竞争线程数超过CPU核心数一半。
3.3 重量级锁(Heavyweight Locking)
设计目标:应对高并发激烈竞争。
实现原理:
- Mark Word指向ObjectMonitor,未获取锁的线程进入
_EntryList
阻塞(操作系统级别的互斥锁)。 - 线程阻塞/唤醒涉及用户态→内核态切换,开销较大。
性能对比:
锁状态 | 获取成本 | 释放成本 | 适用场景 |
---|---|---|---|
偏向锁 | 极低 | 极低 | 单线程重复访问 |
轻量级锁 | 低(CAS) | 低(CAS) | 多线程交替执行 |
重量级锁 | 高 | 高 | 多线程同时竞争 |
四、JDK优化:从锁消除到虚拟线程
4.1 锁优化技术
锁消除(Lock Elimination)
JIT编译器通过逃逸分析,消除不可能存在竞争的锁。例如:
public String concat(String a, String b) {StringBuffer sb = new StringBuffer(); // StringBuffer的append是同步方法sb.append(a).append(b);return sb.toString();
}
// 逃逸分析发现sb未逃逸,消除同步锁
锁粗化(Lock Coarsening)
合并连续的锁申请,减少锁竞争频率:
for (int i = 0; i < 1000; i++) {synchronized (lock) { // 循环内频繁加锁,粗化为一次锁申请count++;}
}
// 优化后:synchronized (lock) { for (...) { count++; } }
自适应自旋(Adaptive Spinning)
JVM根据历史自旋成功率动态调整次数:
- 若自旋成功,下次增加自旋次数(最大100次)。
- 若自旋失败,减少或省略自旋,避免CPU空转。
4.2 JDK 17的虚拟线程支持
JDK 17通过JEP 491优化synchronized
与虚拟线程(Virtual Threads)的兼容性,避免线程固定(Pinning):
- 虚拟线程阻塞于
synchronized
时,JVM自动卸载载体线程(Carrier Thread),允许其他虚拟线程复用。 - 实现原理:结合
Continuation
机制,在阻塞时保存栈帧,释放载体线程。
性能提升:在高并发I/O场景,吞吐量提升30%+,避免传统线程阻塞导致的资源浪费。
五、源码深度剖析:ObjectMonitor关键方法
5.1 加锁(enter方法)
void ATTR ObjectMonitor::enter(TRAPS) {Thread* Self = THREAD;void* cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL); // CAS尝试获取锁if (cur == NULL) { // 成功获取锁,_owner = Selfreturn;}if (cur == Self) { // 重入,_recursions++_recursions++;return;}// 竞争失败,进入自旋或阻塞if (Knob_SpinEarly && TrySpin(Self) > 0) { // 自旋成功,获取锁return;}// 自旋失败,进入_EntryList阻塞ThreadBlockInVM tbivm(Self);Self->set_current_pending_monitor(this);EnterI(Self); // 进入阻塞队列
}
5.2 释放锁(exit方法)
void ATTR ObjectMonitor::exit(TRAPS) {if (_recursions != 0) { // 重入解锁,_recursions--_recursions--;return;}// 唤醒_EntryList中的线程ObjectWaiter* w = NULL;w = _EntryList;if (w != NULL) {Atomic::cmpxchg_ptr(NULL, &_owner, Self); // 释放锁OrderAccess::fence();WakeupWaiter(w); // 唤醒线程return;}// 无等待线程,直接释放Atomic::cmpxchg_ptr(NULL, &_owner, Self);
}
六、性能对比与最佳实践
6.1 synchronized vs Lock(ReentrantLock)
特性 | synchronized | ReentrantLock |
---|---|---|
实现层级 | JVM层面(关键字) | JDK层面(接口) |
锁释放 | 自动释放(异常/正常退出) | 手动释放(需finally块) |
高级功能 | 无(不可中断、非公平) | 支持中断、超时、公平锁、条件变量 |
性能(低竞争) | 接近(偏向锁/轻量级锁) | 略高(CAS操作) |
性能(高竞争) | 重量级锁开销大 | 略优(队列优化) |
建议:简单同步需求用synchronized
(简洁安全);需高级功能(如超时获取)用ReentrantLock
。
6.2 生产环境最佳实践
-
减小锁粒度:同步代码块仅包裹临界区,避免锁范围过大:
// 反例:整个方法加锁 public synchronized void process() {readConfig(); // 无需同步的操作updateState(); // 需同步的临界区 } // 正例:仅临界区加锁 public void process() {readConfig();synchronized (lock) {updateState();} }
-
避免嵌套锁:减少死锁风险,如必须嵌套,确保锁顺序一致。
-
禁用偏向锁:高并发场景下,偏向锁撤销开销大,通过
-XX:-UseBiasedLocking
禁用。 -
监控锁状态:使用
jstack
查看线程阻塞状态,定位竞争热点:jstack <pid> | grep -A 20 "BLOCKED" # 查看阻塞线程
-
JVM参数调优:
-XX:PreBlockSpin=10
:轻量级锁自旋次数。-XX:BiasedLockingBulkRebiasThreshold=20
:批量重偏向阈值。-XX:BiasedLockingBulkRevokeThreshold=40
:批量撤销阈值。
七、总结
synchronized
从早期的重量级锁进化为如今的自适应锁机制,体现了JVM对性能的极致追求。其核心在于动态锁升级(偏向锁→轻量级锁→重量级锁),结合对象头Mark Word与ObjectMonitor实现高效同步。在JDK 17中,通过对虚拟线程的支持,进一步提升了高并发场景下的可扩展性。
掌握synchronized
的底层原理,不仅能写出更高效的并发代码,更能深入理解JVM的优化机制。在实际开发中,需结合业务场景选择合适的锁策略,平衡安全性与性能,避免过度同步或锁竞争导致的性能瓶颈。
参考资料:
- OpenJDK源码(hotspot/src/share/vm/runtime/objectMonitor.hpp)
- 《深入理解Java虚拟机》(周志明)
- JDK官方文档(JEP 142、JEP 491)