synchronized 锁分析
一、synchronized 基础概述
synchronized 是 Java 中最基本的同步机制,用于实现线程间的同步和互斥访问,确保在多线程环境下共享资源的一致性和可见性。
1.1 synchronized 的核心作用
- 互斥性:确保同一时刻只有一个线程可以进入同步代码块
- 可见性:保证线程释放锁之前对共享变量的修改对后续获取该锁的线程可见
- 有序性:通过锁的获取和释放建立的happens-before关系,部分解决了指令重排序问题
1.2 synchronized 的使用场景
- 多线程访问共享变量时保证数据一致性
- 实现线程间的同步协作
- 保护临界区资源,防止竞态条件
二、synchronized 的使用方式
synchronized 可以以多种形式使用,适用于不同的同步需求场景。
2.1 修饰实例方法
当 synchronized 修饰实例方法时,锁是当前对象实例(this)。
public synchronized void instanceMethod() {
// 同步代码块
// 同一时间只有一个线程可以执行此方法
}特点:
- 不同实例对象的同步方法互不干扰
- 同一实例的多个同步方法共享同一把锁
2.2 修饰静态方法
当 synchronized 修饰静态方法时,锁是当前类的 Class 对象。
public static synchronized void staticMethod() {
// 同步代码块
// 同一时间只有一个线程可以执行此类的任何静态同步方法
}特点:
- 所有此类的实例共享同一把锁
- 静态同步方法和实例同步方法使用不同的锁,互不干扰
2.3 修饰代码块
当 synchronized 修饰代码块时,锁是括号中指定的对象。这是最灵活的使用方式。
public void codeBlock() {
// 非同步代码
synchronized (this) {
// 实例锁同步代码块
}
synchronized (SynchronizedDemo.class) {
// 类锁同步代码块
}
Object lock = new Object();
synchronized (lock) {
// 任意对象锁同步代码块
}
}特点:
- 可以精确控制同步范围,减少锁的持有时间
- 可以选择任意对象作为锁,实现更细粒度的控制
- 常用于只需要同步代码的特定部分而非整个方法的场景
三、synchronized 的实现原理
3.1 基于 Monitor 的实现
synchronized 的实现基于 JVM 内部的监视器锁(Monitor)机制。Monitor 是一种同步机制,它确保在同一时间只有一个线程可以进入被保护的代码区域。
在 Java 中,每个对象都有一个内置的 Monitor 锁,也称为对象锁。当线程需要访问同步代码时,它必须先获取该对象的 Monitor 锁。
3.2 字节码层面的实现
在字节码层面,synchronized 是通过 monitorenter 和 monitorexit 指令来实现的。
monitorenter:尝试获取对象的 Monitor 锁,如果获取成功则进入同步代码块monitorexit:释放对象的 Monitor 锁
对于同步方法,JVM 使用 ACC_SYNCHRONIZED 标志来实现同步,而非显式的 monitorenter 和 monitorexit 指令。
3.3 Java 对象头与锁状态
Java 对象在内存中的布局包括:对象头、实例数据和对齐填充。其中,对象头包含了锁状态信息。
对象头的结构(以 HotSpot VM 为例):
- Mark Word:存储对象的哈希码、GC 分代年龄、锁状态等信息
- Klass Pointer:指向对象的类元数据的指针
Mark Word 的结构会根据对象的锁状态发生变化,主要包括以下几种状态:
- 无锁状态:普通对象的状态
- 偏向锁状态:适用于单线程反复获取同一把锁的场景
- 轻量级锁状态:适用于线程交替执行同步代码的场景
- 重量级锁状态:适用于多线程同时竞争同一把锁的场景
四、synchronized 的锁优化机制
JDK 6 及以后版本对 synchronized 进行了大量优化,引入了锁升级机制,以提高并发性能。
4.1 偏向锁
目的:减少单线程环境下获取锁的开销
工作原理:
- 当线程第一次获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程 ID
- 之后该线程再次获取锁时,只需检查对象头中的线程 ID 是否为当前线程 ID,无需进行 CAS 操作
- 当有其他线程尝试获取该锁时,偏向锁会升级为轻量级锁
启用/禁用:
- JDK 6 及以后版本默认启用偏向锁
- 可以通过 JVM 参数
-XX:+UseBiasedLocking(启用)或-XX:-UseBiasedLocking(禁用)控制
4.2 轻量级锁
目的:减少多线程交替执行同步代码时的性能消耗
工作原理:
- 线程在执行同步代码前,先在栈帧中创建锁记录(Lock Record)
- 然后尝试使用 CAS 将对象头中的 Mark Word 复制到锁记录中,并将对象头设置为指向锁记录的指针
- 如果 CAS 成功,表示获取锁成功;如果失败,表示有其他线程竞争,此时轻量级锁会升级为重量级锁
4.3 重量级锁
目的:解决多线程同时竞争锁的情况
工作原理:
- 当轻量级锁升级为重量级锁后,对象头中的 Mark Word 会指向一个互斥量(mutex)
- 线程获取重量级锁失败时,会被阻塞并放入锁的等待队列
- 当持有锁的线程释放锁时,会唤醒等待队列中的线程,重新竞争锁
4.4 锁的升级路径
synchronized 锁的升级是单向的,只能从无锁 → 偏向锁 → 轻量级锁 → 重量级锁,不能反向降级。

4.5 其他优化
- 锁消除:JVM 分析代码发现某些锁对象不会被多线程访问,就会消除这些锁
- 锁粗化:将多个连续的锁合并为一个范围更大的锁,减少锁的获取和释放开销
- 自旋锁:线程在获取锁失败时,不会立即阻塞,而是执行一段自旋操作,可能在锁释放前就获得锁
- 自适应自旋:根据上一次自旋的结果动态调整自旋次数
五、synchronized 与其他同步机制的比较
5.1 synchronized 与 ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取方式 | 隐式获取和释放 | 显式通过 lock()/unlock() 获取和释放 |
| 可中断性 | 不可中断 | 可中断(通过 lockInterruptibly()) |
| 公平性 | 非公平 | 可选择公平或非公平 |
| 条件变量 | 不支持 | 支持(通过 Condition) |
| 锁释放 | 自动释放(即使发生异常) | 必须手动释放(通常在 finally 块中) |
| 性能 | JDK 6 后性能大幅提升,与 ReentrantLock 接近 | 在高并发下性能略好 |
5.2 synchronized 与 volatile
| 特性 | synchronized | volatile |
|---|---|---|
| 互斥性 | 支持 | 不支持 |
| 可见性 | 支持 | 支持 |
| 有序性 | 支持(部分) | 支持 |
| 原子性 | 支持 | 不支持 |
| 适用场景 | 多线程竞争的代码块 | 简单的状态标志 |
六、synchronized 的使用最佳实践
6.1 减少锁的持有时间
尽量减少同步代码块的范围,只包含必要的共享资源访问部分,以提高并发性能。
// 不推荐
public synchronized void process() {
// 非共享资源操作
doSomething();
// 共享资源操作
updateSharedState();
}
// 推荐
public void process() {
// 非共享资源操作
doSomething();
// 只同步共享资源部分
synchronized (this) {
updateSharedState();
}
}6.2 避免死锁
死锁发生的四个必要条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件。
避免死锁的策略:
- 按固定顺序获取锁
- 避免嵌套锁
- 设置锁的超时时间
- 使用 ReentrantLock 替代 synchronized,可以通过 lockInterruptibly() 中断
6.3 选择合适的锁粒度
- 对象锁:适用于保护实例级别的共享资源
- 类锁:适用于保护静态共享资源
- 私有锁对象:提供更好的封装性和灵活性
public class OptimizedSynchronized {
// 私有锁对象,提供更好的封装性
private final Object lock = new Object();
public void method1() {
synchronized (lock) {
// 同步代码
}
}
public void method2() {
synchronized (lock) {
// 同步代码
}
}
}6.4 避免使用字符串常量作为锁对象
字符串常量在 JVM 中是共享的,可能导致意外的锁竞争。
// 不推荐
public void method() {
synchronized ("lock") {
// 同步代码
}
}
// 推荐
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// 同步代码
}
}6.5 了解锁的升级机制
根据实际的并发场景,合理利用 JVM 的锁优化机制:
- 单线程场景:偏向锁会自动生效
- 低竞争场景:轻量级锁提供更好的性能
- 高竞争场景:重量级锁保证线程安全
七、synchronized 的常见问题与解决方案
7.1 性能问题
问题:synchronized 在高并发场景下可能成为性能瓶颈
解决方案:
- 减少锁的持有时间
- 降低锁的粒度(如使用 ConcurrentHashMap 替代 Hashtable)
- 使用读写锁分离(如 ReentrantReadWriteLock)
- 考虑使用无锁数据结构(如 Atomic 类)
7.2 线程阻塞
问题:持有锁的线程被阻塞可能导致其他线程长时间等待
解决方案:
- 避免在同步代码中执行阻塞操作
- 考虑使用非阻塞算法
- 使用 ReentrantLock 并设置超时时间
7.3 无法中断等待锁的线程
问题:synchronized 不支持中断等待锁的线程
解决方案:
- 使用 ReentrantLock 的 lockInterruptibly() 方法替代
- 设计更合理的锁获取顺序,避免长时间等待
八、总结
synchronized 是 Java 并发编程中最基础也是最重要的同步机制之一。尽管在早期版本中性能表现不佳,但经过 JDK 6 及以后版本的优化,其性能已经得到了显著提升,适用于大多数并发场景。
在实际开发中,我们应当根据具体的并发需求和场景特点,选择合适的同步机制,并遵循最佳实践,以实现高效、安全的并发程序。
九、Java 8 到 Java 23 中 synchronized 的演变
自 Java 8 以来,虽然 synchronized 机制的核心原理保持稳定,但 Java 平台在不同版本中对其进行了一系列优化和调整,以适应现代应用的需求。
9.1 Java 8 (2014)
Java 8 作为一个重要的长期支持版本(LTS),对 synchronized 机制主要进行了性能优化:
- 改进了 JVM 内部的锁调度算法
- 对偏向锁和轻量级锁的实现进行了优化
- 增强了对多核处理器的支持
9.2 Java 9 到 Java 14 (2017-2020)
这几个版本主要对 synchronized 进行了渐进式的性能优化:
- 改进了锁的获取和释放路径
- 优化了线程调度策略
- 增强了 JVM 对锁竞争场景的处理能力
9.3 Java 15 (2020)
Java 15 中一个重要的变化是 移除了偏向锁(JEP 374):
- 偏向锁最初设计是为了提高单线程环境下的性能
- 但现代应用通常有更复杂的并发模式,偏向锁的收益逐渐降低
- 移除偏向锁简化了 JVM 的代码库,减少了维护成本
- 对于大多数应用,移除偏向锁不会导致可观测的性能变化
9.4 Java 16 到 Java 18 (2021-2022)
这些版本继续对 synchronized 进行性能优化:
- 进一步优化了重量级锁的实现
- 改进了线程阻塞和唤醒机制
- 增强了锁在不同 CPU 架构上的适应性
9.5 Java 19 (2022)
Java 19 引入了 虚拟线程(JEP 425,预览特性),这对并发编程产生了深远影响:
- 虚拟线程是轻量级线程,由 JVM 调度而非操作系统
- 大量虚拟线程可以映射到少量操作系统线程上
- 虚拟线程在阻塞操作时会自动释放底层操作系统线程,提高资源利用率
- 虽然
synchronized本身的实现没有变化,但在虚拟线程环境中的行为特性有所不同 - 虚拟线程在持有
synchronized锁时可能会阻塞底层线程,影响其他虚拟线程
9.6 Java 20 (2023)
Java 20 中,虚拟线程特性从预览状态演进为第二预览版,但对 synchronized 的直接影响不大。
9.7 Java 21 (2023)
Java 21 作为一个 LTS 版本,有几个重要变化:
- 虚拟线程正式转正(JEP 444),成为生产环境可用的特性
- 结构化并发(JEP 443,预览特性)引入,提供了更清晰的并发编程模型
- 对
synchronized在虚拟线程环境下的性能进行了进一步优化
9.8 Java 22 (2024)
Java 22 对 synchronized 的主要改进是在虚拟线程环境下的优化:
- 减少了
synchronized锁在虚拟线程上下文中的开销 - 优化了虚拟线程持有
synchronized锁时的调度行为
9.9 Java 23 (2024)
Java 23 继续优化 synchronized 的性能:
- 进一步改进了锁的竞争处理机制
- 优化了 JVM 在高并发场景下的内存管理
- 增强了对新型硬件架构的适应性
9.10 未来发展趋势
从 Java 8 到 Java 23 的演变可以看出,synchronized 的发展趋势主要是:
- 性能持续优化:每个版本都在不断改进锁的实现和调度算法
- 简化实现:移除了偏向锁等不再适合现代应用的优化
- 适应新的并发模型:针对虚拟线程等新技术进行适配和优化
- 保持向后兼容性:核心 API 和语义保持稳定,确保现有代码能够正常运行
尽管引入了 ReentrantLock、StampedLock 等更灵活的同步机制,synchronized 仍然是 Java 并发编程中最基础、最广泛使用的同步机制之一,其简洁的语法和可靠的行为使其在许多场景下仍然是首选方案。
