synchronized关键字:Java 中的同步机制与锁升级原理
synchronized是 Java 中用于保证线程安全的核心关键字,通过实现同步互斥来避免多线程并发访问共享资源时的竞态条件。随着 JVM 的演进,synchronized从早期的 “重量级锁” 发展为具备锁升级能力的高效同步机制。本文将深入解析其作用、实现原理及锁升级过程。
synchronized 的核心作用
synchronized的核心目标是解决多线程并发问题,具体通过以下特性实现:
- 原子性:保证临界区代码(被
synchronized修饰的代码块或方法)同一时刻只能被一个线程执行,确保操作不可分割; - 可见性:通过 JMM(Java 内存模型)的规则,确保一个线程对共享变量的修改能被其他线程立即看到;
- 有序性:禁止指令重排序,保证临界区代码按顺序执行。
synchronized 的使用方式
synchronized可修饰代码块或方法,其锁对象的选择决定了同步粒度:
修饰实例方法
锁对象为当前实例(this),同一实例的多个synchronized方法共享同一把锁。
1 | public synchronized void method() { |
修饰静态方法
锁对象为当前类的 Class 对象(如MyClass.class),所有静态synchronized方法共享同一把锁。
1 | public static synchronized void staticMethod() { |
修饰代码块
显式指定锁对象,灵活性更高,可缩小同步范围。
1 | // 锁实例对象 |
synchronized 的实现原理
synchronized的底层依赖JVM 指令和对象头实现,核心是通过 “监视器锁(monitor)” 控制线程访问。
字节码层面:monitorenter 与 monitorexit
当代码被synchronized修饰时,JVM 会在编译时生成两个指令:
- monitorenter:进入同步块时执行,尝试获取锁(将锁计数器 + 1);
- monitorexit:退出同步块时执行,释放锁(将锁计数器 - 1,计数器为 0 时锁被释放)。
示例字节码(简化版):
1 | // 进入同步块 |
底层依赖:监视器(monitor)
监视器(monitor)是操作系统层面的互斥锁(Mutex Lock)的封装,用于控制线程对临界区的访问。线程必须获取 monitor 的所有权才能执行临界区代码:
- 一个 monitor 只能被一个线程持有;
- 其他线程尝试获取时会被阻塞,进入等待队列;
- 持有线程释放 monitor 后,等待队列中的线程被唤醒并竞争锁。
锁的存储:对象头(Mark Word)
Java 对象在内存中的布局包括对象头、实例数据和对齐填充。其中,对象头的 Mark Word是实现synchronized锁的关键,用于存储锁状态信息。
Mark Word 的结构随锁状态动态变化(32 位 JVM 示例):
| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | 对象哈希码、分代年龄 | 01 |
| 偏向锁 | 偏向线程 ID、偏向时间戳、分代年龄 | 01 |
| 轻量级锁 | 指向栈中锁记录(Lock Record)的指针 | 00 |
| 重量级锁 | 指向监视器(monitor)的指针 | 10 |
锁升级:从低效到高效的演进
JDK 1.6 为优化synchronized性能,引入了锁升级机制:锁状态从无锁逐步升级为偏向锁、轻量级锁、重量级锁,且只能升级不能降级。这一过程根据线程竞争强度动态调整,平衡性能与安全性。
1. 无锁状态
- 场景:对象刚创建,无线程访问同步块。
- Mark Word:存储对象哈希码和分代年龄,标志位为
01,偏向锁标志为0。
2. 偏向锁:单线程无竞争场景
核心思想:若同步块长期被同一线程访问,直接 “偏向” 该线程,避免每次加锁 / 解锁的开销。
加锁过程
- 线程首次访问同步块时,检查 Mark Word 是否为 “可偏向状态”(偏向标志
1且线程 ID 为空); - 通过 CAS 操作将当前线程 ID 写入 Mark Word,标志位保持
01(偏向锁标志1); - 后续该线程再次进入同步块时,仅需对比线程 ID,无需 CAS 操作。
撤销偏向锁
当其他线程尝试竞争锁时,偏向锁需撤销并升级:
- 暂停持有偏向锁的线程(需等待全局安全点,即无字节码执行的时刻);
- 检查持有线程状态:
- 若线程已结束,将对象头重置为无锁状态;
- 若线程仍在执行,升级为轻量级锁。
3. 轻量级锁:多线程交替访问场景
核心思想:多线程交替访问同步块时,通过自旋(忙等)避免线程阻塞,减少上下文切换开销。
加锁过程
- 线程在栈帧中创建锁记录(Lock Record),拷贝对象头的 Mark Word 到锁记录;
- 通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针(标志位
00); - 若 CAS 成功,线程获取轻量级锁;若失败,说明存在竞争,进入自旋。
解锁过程
- 通过 CAS 操作将锁记录中的 Mark Word(原始对象头)写回对象头;
- 若 CAS 成功,锁释放;若失败,说明存在竞争,轻量级锁升级为重量级锁。
4. 重量级锁:多线程激烈竞争场景
核心思想:当自旋无法获取锁(如竞争激烈或同步块执行时间长),升级为依赖操作系统互斥锁的重量级锁,避免自旋消耗 CPU。
加锁过程
- 线程获取锁失败后,放弃自旋,进入 monitor 的等待队列;
- 线程状态转为阻塞(BLOCKED),释放 CPU 资源;
- 持有锁的线程释放锁后,唤醒等待队列中的线程竞争锁。
性能特点
- 优点:无自旋消耗,适合长同步块;
- 缺点:线程阻塞 / 唤醒需切换内核态,开销大。
5. 锁升级完整流程
1 | 无锁 → 偏向锁(单线程) → 轻量级锁(多线程交替) → 重量级锁(激烈竞争) |
触发升级的关键场景:
- 偏向锁 → 轻量级锁:其他线程竞争锁;
- 轻量级锁 → 重量级锁:自旋失败(如自旋次数超限或新增竞争线程)。
synchronized 的可见性保证
synchronized通过 JMM 的规则保证可见性:
- 解锁前:线程必须将工作内存中修改的共享变量同步到主内存;
- 加锁时:线程清空工作内存中该变量的值,从主内存重新加载最新值。
总结与最佳实践
性能对比
| 锁状态 | 适用场景 | 性能特点 |
|---|---|---|
| 偏向锁 | 单线程访问 | 几乎无开销 |
| 轻量级锁 | 多线程交替访问 | 自旋开销小,无阻塞 |
| 重量级锁 | 多线程激烈竞争或长同步块 | 阻塞开销大,适合高吞吐量 |
最佳实践
- 缩小同步范围:使用同步块而非同步方法,减少锁竞争;
- 避免嵌套锁:防止死锁和锁升级;
- 结合 JVM 参数:如
-XX:+UseBiasedLocking(启用偏向锁)、-XX:PreBlockSpin(调整自旋次数)。