跳到主要内容

synchronized 关键字

问题

synchronized 的实现原理是什么?锁升级过程是怎样的?synchronized 和 Lock 有什么区别?

答案

synchronized 的三种用法

SynchronizedUsage.java
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 可重入

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)

适用于只有一个线程访问同步块的场景:

  1. 线程首次进入同步块 → 通过 CAS 将线程 ID 写入 Mark Word
  2. 后续进入 → 检查 Mark Word 中的线程 ID 是否是自己,无需 CAS
  3. 如果是自己 → 直接进入(几乎零开销)
  4. 如果不是 → 偏向锁撤销,升级为轻量级锁
JDK 15 废弃偏向锁

JDK 15 默认关闭偏向锁(-XX:-UseBiasedLocking),JDK 18 彻底移除。原因:现代应用中多线程竞争是常态,偏向锁的撤销开销(STW)反而成为负担。

轻量级锁(Lightweight Locking)

适用于线程交替执行(无实际竞争)的场景:

  1. 在当前线程栈帧中创建 Lock Record(锁记录)
  2. 将 Mark Word 拷贝到 Lock Record(Displaced Mark Word)
  3. 通过 CAS 尝试将 Mark Word 指向 Lock Record
  4. CAS 成功 → 获取轻量级锁
  5. CAS 失败 → 自旋重试(适应性自旋)
  6. 自旋超过阈值 → 膨胀为重量级锁

重量级锁(Heavyweight Locking)

多线程激烈竞争时使用:

  1. 关联一个 ObjectMonitor 对象
  2. 未获取到锁的线程进入 _EntryList 阻塞(用户态 → 内核态切换)
  3. 锁释放后,从 _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 的区别?

答案

对比synchronizedReentrantLock
实现层面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 废弃了偏向锁?

答案

  1. 偏向锁的撤销需要 STW(Stop The World),成本高
  2. 现代应用以多线程为主,偏向锁的优化场景(单线程独占)较少
  3. 偏向锁的实现增加了 JVM 代码复杂度
  4. 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 才真正释放锁。

相关链接