0%

synchronized关键字

synchronized关键字:Java 中的同步机制与锁升级原理

synchronized是 Java 中用于保证线程安全的核心关键字,通过实现同步互斥来避免多线程并发访问共享资源时的竞态条件。随着 JVM 的演进,synchronized从早期的 “重量级锁” 发展为具备锁升级能力的高效同步机制。本文将深入解析其作用、实现原理及锁升级过程。

synchronized 的核心作用

synchronized的核心目标是解决多线程并发问题,具体通过以下特性实现:

  1. 原子性:保证临界区代码(被synchronized修饰的代码块或方法)同一时刻只能被一个线程执行,确保操作不可分割;
  2. 可见性:通过 JMM(Java 内存模型)的规则,确保一个线程对共享变量的修改能被其他线程立即看到;
  3. 有序性:禁止指令重排序,保证临界区代码按顺序执行。

synchronized 的使用方式

synchronized可修饰代码块或方法,其锁对象的选择决定了同步粒度:

修饰实例方法

锁对象为当前实例(this),同一实例的多个synchronized方法共享同一把锁。

1
2
3
public synchronized void method() {  
// 临界区代码
}

修饰静态方法

锁对象为当前类的 Class 对象(如MyClass.class),所有静态synchronized方法共享同一把锁。

1
2
3
public static synchronized void staticMethod() {  
// 临界区代码
}

修饰代码块

显式指定锁对象,灵活性更高,可缩小同步范围。

1
2
3
4
5
6
7
8
9
// 锁实例对象  
synchronized (this) {
// 临界区代码
}

// 锁类对象
synchronized (MyClass.class) {
// 临界区代码
}

synchronized 的实现原理

synchronized的底层依赖JVM 指令对象头实现,核心是通过 “监视器锁(monitor)” 控制线程访问。

字节码层面:monitorenter 与 monitorexit

当代码被synchronized修饰时,JVM 会在编译时生成两个指令:

  • monitorenter:进入同步块时执行,尝试获取锁(将锁计数器 + 1);
  • monitorexit:退出同步块时执行,释放锁(将锁计数器 - 1,计数器为 0 时锁被释放)。

示例字节码(简化版):

1
2
3
4
5
6
7
8
// 进入同步块  
0: aload_0
1: monitorenter
// 临界区代码...
// 退出同步块
7: aload_0
8: monitorexit
9: return

底层依赖:监视器(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. 偏向锁:单线程无竞争场景

核心思想:若同步块长期被同一线程访问,直接 “偏向” 该线程,避免每次加锁 / 解锁的开销。

加锁过程
  1. 线程首次访问同步块时,检查 Mark Word 是否为 “可偏向状态”(偏向标志1且线程 ID 为空);
  2. 通过 CAS 操作将当前线程 ID 写入 Mark Word,标志位保持01(偏向锁标志1);
  3. 后续该线程再次进入同步块时,仅需对比线程 ID,无需 CAS 操作。
撤销偏向锁

当其他线程尝试竞争锁时,偏向锁需撤销并升级:

  1. 暂停持有偏向锁的线程(需等待全局安全点,即无字节码执行的时刻);
  2. 检查持有线程状态:
    • 若线程已结束,将对象头重置为无锁状态;
    • 若线程仍在执行,升级为轻量级锁。

3. 轻量级锁:多线程交替访问场景

核心思想:多线程交替访问同步块时,通过自旋(忙等)避免线程阻塞,减少上下文切换开销。

加锁过程
  1. 线程在栈帧中创建锁记录(Lock Record),拷贝对象头的 Mark Word 到锁记录;
  2. 通过 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针(标志位00);
  3. 若 CAS 成功,线程获取轻量级锁;若失败,说明存在竞争,进入自旋。
解锁过程
  1. 通过 CAS 操作将锁记录中的 Mark Word(原始对象头)写回对象头;
  2. 若 CAS 成功,锁释放;若失败,说明存在竞争,轻量级锁升级为重量级锁。

4. 重量级锁:多线程激烈竞争场景

核心思想:当自旋无法获取锁(如竞争激烈或同步块执行时间长),升级为依赖操作系统互斥锁的重量级锁,避免自旋消耗 CPU。

加锁过程
  1. 线程获取锁失败后,放弃自旋,进入 monitor 的等待队列;
  2. 线程状态转为阻塞(BLOCKED),释放 CPU 资源;
  3. 持有锁的线程释放锁后,唤醒等待队列中的线程竞争锁。
性能特点
  • 优点:无自旋消耗,适合长同步块;
  • 缺点:线程阻塞 / 唤醒需切换内核态,开销大。

5. 锁升级完整流程

1
无锁 → 偏向锁(单线程) → 轻量级锁(多线程交替) → 重量级锁(激烈竞争)  

触发升级的关键场景

  • 偏向锁 → 轻量级锁:其他线程竞争锁;
  • 轻量级锁 → 重量级锁:自旋失败(如自旋次数超限或新增竞争线程)。

synchronized 的可见性保证

synchronized通过 JMM 的规则保证可见性:

  1. 解锁前:线程必须将工作内存中修改的共享变量同步到主内存;
  2. 加锁时:线程清空工作内存中该变量的值,从主内存重新加载最新值。

总结与最佳实践

性能对比

锁状态 适用场景 性能特点
偏向锁 单线程访问 几乎无开销
轻量级锁 多线程交替访问 自旋开销小,无阻塞
重量级锁 多线程激烈竞争或长同步块 阻塞开销大,适合高吞吐量

最佳实践

  • 缩小同步范围:使用同步块而非同步方法,减少锁竞争;
  • 避免嵌套锁:防止死锁和锁升级;
  • 结合 JVM 参数:如-XX:+UseBiasedLocking(启用偏向锁)、-XX:PreBlockSpin(调整自旋次数)。

欢迎关注我的其它发布渠道