Skip to content

synchronized 锁分析

一、synchronized 基础概述

synchronized 是 Java 中最基本的同步机制,用于实现线程间的同步和互斥访问,确保在多线程环境下共享资源的一致性和可见性。

1.1 synchronized 的核心作用

  • 互斥性:确保同一时刻只有一个线程可以进入同步代码块
  • 可见性:保证线程释放锁之前对共享变量的修改对后续获取该锁的线程可见
  • 有序性:通过锁的获取和释放建立的happens-before关系,部分解决了指令重排序问题

1.2 synchronized 的使用场景

  • 多线程访问共享变量时保证数据一致性
  • 实现线程间的同步协作
  • 保护临界区资源,防止竞态条件

二、synchronized 的使用方式

synchronized 可以以多种形式使用,适用于不同的同步需求场景。

2.1 修饰实例方法

synchronized 修饰实例方法时,锁是当前对象实例(this)。

java
public synchronized void instanceMethod() {
    // 同步代码块
    // 同一时间只有一个线程可以执行此方法
}

特点

  • 不同实例对象的同步方法互不干扰
  • 同一实例的多个同步方法共享同一把锁

2.2 修饰静态方法

synchronized 修饰静态方法时,锁是当前类的 Class 对象。

java
public static synchronized void staticMethod() {
    // 同步代码块
    // 同一时间只有一个线程可以执行此类的任何静态同步方法
}

特点

  • 所有此类的实例共享同一把锁
  • 静态同步方法和实例同步方法使用不同的锁,互不干扰

2.3 修饰代码块

synchronized 修饰代码块时,锁是括号中指定的对象。这是最灵活的使用方式。

java
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 是通过 monitorentermonitorexit 指令来实现的。

  • monitorenter:尝试获取对象的 Monitor 锁,如果获取成功则进入同步代码块
  • monitorexit:释放对象的 Monitor 锁

对于同步方法,JVM 使用 ACC_SYNCHRONIZED 标志来实现同步,而非显式的 monitorentermonitorexit 指令。

3.3 Java 对象头与锁状态

Java 对象在内存中的布局包括:对象头、实例数据和对齐填充。其中,对象头包含了锁状态信息。

对象头的结构(以 HotSpot VM 为例):

  • Mark Word:存储对象的哈希码、GC 分代年龄、锁状态等信息
  • Klass Pointer:指向对象的类元数据的指针

Mark Word 的结构会根据对象的锁状态发生变化,主要包括以下几种状态:

  1. 无锁状态:普通对象的状态
  2. 偏向锁状态:适用于单线程反复获取同一把锁的场景
  3. 轻量级锁状态:适用于线程交替执行同步代码的场景
  4. 重量级锁状态:适用于多线程同时竞争同一把锁的场景

四、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

特性synchronizedReentrantLock
获取方式隐式获取和释放显式通过 lock()/unlock() 获取和释放
可中断性不可中断可中断(通过 lockInterruptibly())
公平性非公平可选择公平或非公平
条件变量不支持支持(通过 Condition)
锁释放自动释放(即使发生异常)必须手动释放(通常在 finally 块中)
性能JDK 6 后性能大幅提升,与 ReentrantLock 接近在高并发下性能略好

5.2 synchronized 与 volatile

特性synchronizedvolatile
互斥性支持不支持
可见性支持支持
有序性支持(部分)支持
原子性支持不支持
适用场景多线程竞争的代码块简单的状态标志

六、synchronized 的使用最佳实践

6.1 减少锁的持有时间

尽量减少同步代码块的范围,只包含必要的共享资源访问部分,以提高并发性能。

java
// 不推荐
public synchronized void process() {
    // 非共享资源操作
    doSomething();
    // 共享资源操作
    updateSharedState();
}

// 推荐
public void process() {
    // 非共享资源操作
    doSomething();
    // 只同步共享资源部分
    synchronized (this) {
        updateSharedState();
    }
}

6.2 避免死锁

死锁发生的四个必要条件:互斥条件、请求与保持条件、不剥夺条件、循环等待条件。

避免死锁的策略

  • 按固定顺序获取锁
  • 避免嵌套锁
  • 设置锁的超时时间
  • 使用 ReentrantLock 替代 synchronized,可以通过 lockInterruptibly() 中断

6.3 选择合适的锁粒度

  • 对象锁:适用于保护实例级别的共享资源
  • 类锁:适用于保护静态共享资源
  • 私有锁对象:提供更好的封装性和灵活性
java
public class OptimizedSynchronized {
    // 私有锁对象,提供更好的封装性
    private final Object lock = new Object();
    
    public void method1() {
        synchronized (lock) {
            // 同步代码
        }
    }
    
    public void method2() {
        synchronized (lock) {
            // 同步代码
        }
    }
}

6.4 避免使用字符串常量作为锁对象

字符串常量在 JVM 中是共享的,可能导致意外的锁竞争。

java
// 不推荐
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 的发展趋势主要是:

  1. 性能持续优化:每个版本都在不断改进锁的实现和调度算法
  2. 简化实现:移除了偏向锁等不再适合现代应用的优化
  3. 适应新的并发模型:针对虚拟线程等新技术进行适配和优化
  4. 保持向后兼容性:核心 API 和语义保持稳定,确保现有代码能够正常运行

尽管引入了 ReentrantLock、StampedLock 等更灵活的同步机制,synchronized 仍然是 Java 并发编程中最基础、最广泛使用的同步机制之一,其简洁的语法和可靠的行为使其在许多场景下仍然是首选方案。

基于 MIT 许可发布