synchronized 关键字
问题
synchronized 的实现原理是什么?锁升级过程是怎样的?synchronized 和 Lock 有什么区别?
答案
synchronized 的三种用法
public class SynchronizedDemo {
// 1. 修饰实例方法 —— 锁的是当前实例对象 this
public synchronized void instanceMethod() {
// 临界区
}
// 2. 修饰静态方法 —— 锁的是 Class 对象
public static synchronized void staticMethod() {
// 临界区
}
// 3. 修饰代码块 —— 锁的是指定对象
public void blockMethod() {
synchronized (this) { // 锁实例
// 临界区
}
synchronized (SynchronizedDemo.class) { // 锁 Class
// 临界区
}
}
}
两个线程分别访问同一个对象的两个 synchronized 实例方法会互斥(锁同一个 this),但访问不同对象的 synchronized 方法不会互斥(锁不同对象)。
底层实现原理
synchronized 在 JVM 层面基于 Monitor(监视器锁/管程) 实现:
同步代码块:monitorenter / monitorexit
// 编译后的字节码(javap -verbose)
public void blockMethod();
Code:
monitorenter // 进入同步块,获取 monitor
// ... 临界区代码 ...
monitorexit // 正常退出,释放 monitor
// ...
monitorexit // 异常退出,释放 monitor(保证锁一定释放)
同步方法:ACC_SYNCHRONIZED 标志
// 方法的 access_flags 中带有 ACC_SYNCHRONIZED 标志
// JVM 在方法调用时检查该标志,自动获取/释放 monitor
public synchronized void instanceMethod();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Monitor 机制
每个 Java 对象都关联一个 Monitor(由 C++ 的 ObjectMonitor 实现):
- 线程进入 synchronized → 尝试获取 Monitor 的所有权(
_owner) - 获取成功 →
_count + 1,执行临界区代码 - 调用
wait()→ 释放锁,进入_WaitSet - 调用
notify()→ 从_WaitSet中唤醒一个线程到_EntryList - 退出 synchronized →
_count - 1,当_count = 0时释放锁
synchronized 是可重入锁。同一线程再次获取已持有的锁时,_count 递增。释放时递减到 0 才真正释放。避免了同一线程递归调用同步方法时的死锁问题。
锁升级过程(JDK 6+)
JDK 6 引入了锁升级机制,根据竞争程度自动升级,减少不必要的开销:
锁状态存储在对象头的 Mark Word 中(详见 对象内存布局):
| 锁状态 | Mark Word 内容 | 标志位 |
|---|---|---|
| 无锁 | 对象 hashCode、GC 分代年龄 | 01 |
| 偏向锁 | 偏向线程 ID、Epoch、GC 分代年龄 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向 Monitor 对象的指针 | 10 |
| GC 标记 | 空 | 11 |
偏向锁(Biased Locking)
适用于只有一个线程访问同步块的场景:
- 线程首次进入同步块 → 通过 CAS 将线程 ID 写入 Mark Word
- 后续进入 → 检查 Mark Word 中的线程 ID 是否是自己,无需 CAS
- 如果是自己 → 直接进入(几乎零开销)
- 如果不是 → 偏向锁撤销,升级为轻量级锁
JDK 15 默认关闭偏向锁(-XX:-UseBiasedLocking),JDK 18 彻底移除。原因:现代应用中多线程竞争是常态,偏向锁的撤销开销(STW)反而成为负担。
轻量级锁(Lightweight Locking)
适用于线程交替执行(无实际竞争)的场景:
- 在当前线程栈帧中创建 Lock Record(锁记录)
- 将 Mark Word 拷贝到 Lock Record(Displaced Mark Word)
- 通过 CAS 尝试将 Mark Word 指向 Lock Record
- CAS 成功 → 获取轻量级锁
- CAS 失败 → 自旋重试(适应性自旋)
- 自旋超过阈值 → 膨胀为重量级锁
重量级锁(Heavyweight Locking)
多线程激烈竞争时使用:
- 关联一个
ObjectMonitor对象 - 未获取到锁的线程进入
_EntryList阻塞(用户态 → 内核态切换) - 锁释放后,从
_EntryList中唤醒一个线程
完整升级流程
锁优化技术
JVM 对 synchronized 还做了以下优化:
1. 适应性自旋(Adaptive Spinning)
JVM 根据上次自旋结果动态调整自旋次数:
- 上次自旋成功 → 增加自旋次数
- 上次自旋失败 → 减少甚至不自旋
2. 锁消除(Lock Elimination)
JIT 编译器通过逃逸分析发现锁对象不可能被其他线程访问时,消除锁:
// 编译器发现 sb 不会逃逸出方法,会消除 synchronized
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // StringBuffer 内部用 synchronized
sb.append(s1);
sb.append(s2);
return sb.toString();
}
3. 锁粗化(Lock Coarsening)
连续对同一对象加锁/解锁时,合并为一次:
// 优化前:每次 append 都加锁/解锁
sb.append("a"); // lock → unlock
sb.append("b"); // lock → unlock
sb.append("c"); // lock → unlock
// 优化后:合并为一次加锁
// lock
sb.append("a");
sb.append("b");
sb.append("c");
// unlock
常见面试问题
Q1: synchronized 的实现原理?
答案:
synchronized 基于 JVM 的 Monitor(监视器锁) 实现。同步代码块通过 monitorenter/monitorexit 字节码指令实现,同步方法通过方法的 ACC_SYNCHRONIZED 标志实现。底层依赖对象头的 Mark Word 和 ObjectMonitor 数据结构。
Q2: 锁升级的过程是什么?能降级吗?
答案:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁,升级方向不可逆。
- 偏向锁:单线程独占,CAS 写入线程 ID 后无需再次同步
- 轻量级锁:线程交替执行,CAS + 自旋
- 重量级锁:激烈竞争,Monitor 阻塞
严格来说,HotSpot JVM 在 GC 的 STW 阶段可能会对不再使用的重量级锁进行"降级",但这不是传统意义上的降级,面试中回答"不可降级"即可。
Q3: synchronized 和 ReentrantLock 的区别?
答案:
| 对比 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 关键字 | Java API(AQS) |
| 加锁/释放 | 自动 | 手动(必须在 finally 释放) |
| 是否可重入 | 是 | 是 |
| 公平性 | 非公平 | 公平 / 非公平可选 |
| 条件变量 | 只有一个(wait/notify) | 多个 Condition |
| 可中断 | 不可中断 | lockInterruptibly() |
| 超时获取 | 不支持 | tryLock(timeout) |
| 性能 | JDK 6 后优化,差距很小 | 差距很小 |
推荐:简单同步用 synchronized,需要高级功能(公平锁、可中断、超时、多条件)用 ReentrantLock。详见 Lock 接口与 AQS。
Q4: synchronized 修饰实例方法和静态方法的锁对象分别是什么?
答案:
- 实例方法:锁的是当前实例对象
this - 静态方法:锁的是当前类的
Class对象
两者互不影响,因为锁对象不同。一个线程持有实例锁不会阻塞另一个线程获取类锁。
Q5: 什么是锁消除和锁粗化?
答案:
- 锁消除:JIT 编译器通过逃逸分析发现锁对象不会被其他线程访问,直接消除同步操作。例如方法内部创建的 StringBuffer。
- 锁粗化:对同一对象的连续加锁/解锁操作合并为一次,减少不必要的同步开销。
两者都是 JVM 在运行时自动进行的优化。
Q6: 为什么 JDK 15 废弃了偏向锁?
答案:
- 偏向锁的撤销需要 STW(Stop The World),成本高
- 现代应用以多线程为主,偏向锁的优化场景(单线程独占)较少
- 偏向锁的实现增加了 JVM 代码复杂度
- HotSpot 团队测试发现关闭偏向锁后性能反而略有提升
JDK 15 默认关闭,JDK 18 彻底移除偏向锁代码。
Q7: synchronized 是公平锁还是非公平锁?
答案:
synchronized 是非公平锁。当锁被释放时,处于 BLOCKED 状态的线程竞争锁的顺序不由等待时间决定,任何线程都可能获取到锁(包括刚到来的线程)。
非公平锁的优点是吞吐量更高(减少线程切换),缺点是可能造成线程饥饿。
如果需要公平锁,使用 new ReentrantLock(true)。
Q8: 如何理解 synchronized 的可重入性?
答案:
可重入 = 同一线程可以重复获取同一把锁而不会死锁。
public class ReentrantDemo {
public synchronized void methodA() {
methodB(); // 同一线程再次获取 this 锁,不会死锁
}
public synchronized void methodB() {
// ...
}
}
实现原理:Monitor 内部维护 _count 计数器和 _owner 线程。同一线程重入时 _count++,退出时 _count--,减到 0 才真正释放锁。
相关链接
- Java Language Specification - synchronized
- JEP 374: 废弃偏向锁
- 对象内存布局 - Mark Word 与锁状态标志位
- Java 内存模型(JMM) - synchronized 的内存语义
- Lock 接口与 AQS - ReentrantLock 原理
- CAS 与原子类 - 自旋锁底层原理