synchronized 深度解析

540 阅读6分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

synchronized 深度解析

synchronized 是 Java 中实现线程同步的核心机制,通过内置的监视器锁(Monitor)保证代码块或方法的原子性、可见性和有序性。以下从底层实现、锁优化、使用场景及常见问题等方面进行详细分析。


一、底层实现原理

1. 对象头与 Mark Word

  • 对象内存结构
    每个 Java 对象在堆中存储时,其对象头包含两部分:
    • Mark Word(标记字段):存储对象的哈希码、锁状态、GC 分代年龄等。
    • Klass Pointer(类型指针):指向类的元数据。
  • Mark Word 结构(以 64 位 JVM 为例):
    锁状态存储内容
    无锁对象哈希码、分代年龄、是否偏向锁(0)等。
    偏向锁偏向线程 ID、偏向时间戳、分代年龄、锁标志位(01)。
    轻量级锁指向栈中锁记录的指针(Lock Record),锁标志位(00)。
    重量级锁指向监视器(Monitor)的指针,锁标志位(10)。
    GC 标记标记对象是否被垃圾回收,锁标志位(11)。

2. 监视器锁(Monitor)

  • Monitor 结构
    每个对象关联一个 Monitor,包含以下关键字段:

    • Owner:持有锁的线程。
    • EntryList:等待获取锁的阻塞线程队列。
    • WaitSet:调用 wait() 后进入等待状态的线程队列。
  • 锁获取流程

    1. 线程通过 CAS 操作尝试将 Mark Word 指向自己的 Lock Record。
    2. 成功则获得轻量级锁,失败则升级为重量级锁,进入阻塞状态。

3. JVM 指令

  • monitorenter:进入同步代码块,尝试获取锁。
  • monitorexit:退出同步代码块,释放锁。
    public void syncMethod() {
        synchronized (this) { // monitorenter
            // 临界区代码
        } // monitorexit
    }
    

二、锁升级过程

Java 6 后引入锁升级机制,减少锁操作的开销:

image.png

  1. 偏向锁设置

    • 线程首次访问对象时,JVM将线程ID写入Mark Word,进入偏向锁模式。
    • 后续同一线程访问时,直接通过线程ID验证,无需CAS操作。
  2. 偏向锁撤销

    • 当其他线程(ThreadB)尝试获取锁时,JVM检测到偏向锁持有者(ThreadA)未释放,触发偏向锁撤销。
    • Mark Word升级为轻量级锁,记录Lock Record指针。
  3. 轻量级锁竞争

    • 线程通过CAS操作竞争Lock Record指针,失败线程(ThreadB)进入自旋。
    • 自旋超过阈值后,触发锁膨胀(Inflation),升级为重量级锁。
  4. 重量级锁

    • JVM向操作系统申请Monitor对象,Mark Word指向Monitor。
    • 竞争失败的线程(ThreadB)进入阻塞队列(EntryList),由操作系统调度唤醒。

锁升级流程图

        无锁(001)
          │
          ▼
      偏向锁(101)───竞争发生──→ 撤销偏向锁
          │                       │
          ▼                       ▼
      轻量级锁(00)──CAS失败多次─→ 重量级锁(10)

锁升级的意义

  • 性能优化
    根据竞争激烈程度动态调整锁机制,避免无竞争时的性能浪费(偏向锁)和过度竞争时的自旋开销(轻量级锁→重量级锁)。
  • 资源节省
    仅在必要时使用重量级锁(依赖操作系统互斥量),减少内核态切换开销。

三、synchronized 的使用方式

1. 修饰实例方法

  • 锁对象:当前实例(this)。
    public synchronized void instanceMethod() {
        // 临界区代码
    }
    

2. 修饰静态方法

  • 锁对象:类的 Class 对象。
    public static synchronized void staticMethod() {
        // 临界区代码
    }
    

3. 修饰代码块

  • 锁对象:显式指定的任意对象。
    private final Object lock = new Object();
    public void blockMethod() {
        synchronized (lock) {
            // 临界区代码
        }
    }
    

四、锁优化技术

1. 锁消除(Lock Elimination)

  • 原理
    JVM 通过逃逸分析,判断锁对象不会逃逸出当前线程,则直接消除锁。
  • 示例
    public String concat(String s1, String s2) {
        StringBuffer sb = new StringBuffer(); // 对象未逃逸
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
    
    JVM 会自动消除 StringBuffer 内部的同步操作。

2. 锁粗化(Lock Coarsening)

  • 原理
    将连续的多个锁操作合并为单个锁,减少锁获取/释放次数。
    // 优化前
    for (int i = 0; i < 100; i++) {
        synchronized (lock) {
            // 操作
        }
    }
    // 优化后
    synchronized (lock) {
        for (int i = 0; i < 100; i++) {
            // 操作
        }
    }
    

3. 适应性自旋(Adaptive Spinning)

  • 原理
    线程在竞争锁时,先自旋(循环尝试)而非直接阻塞,避免上下文切换。
  • 自旋次数
    JVM 根据历史竞争情况动态调整自旋次数。

五、synchronized 的特性

1. 可重入性(Reentrancy)

  • 原理
    同一线程可多次获取同一把锁,通过 Monitor 的计数器实现。
    public synchronized void methodA() {
        methodB(); // 可重入
    }
    public synchronized void methodB() {
        // ...
    }
    

2. 可见性与有序性

  • 可见性
    锁释放前会将工作内存中的修改刷新到主内存。
  • 有序性
    通过锁保证临界区内的代码按顺序执行(as-if-serial 语义)。

六、常见问题与解决方案

1. 锁竞争激烈

  • 现象
    线程频繁阻塞,CPU 利用率低。
  • 解决
    • 减少锁粒度(如分段锁)。
    • 使用无锁数据结构(如 ConcurrentHashMap)。

2. 死锁

  • 示例
    // 线程1
    synchronized (A) {
        synchronized (B) { ... }
    }
    // 线程2
    synchronized (B) {
        synchronized (A) { ... }
    }
    
  • 解决
    • 按固定顺序获取锁。
    • 使用 tryLock() 设置超时。

3. 误用锁对象

  • 错误示例
    private Integer lock = 1;
    public void syncMethod() {
        synchronized (lock) { // 错误!Integer 对象可能变化(自动装箱)
            // ...
        }
    }
    
  • 解决
    使用 final 修饰锁对象,避免引用变化。

七、synchronized vs ReentrantLock

特性synchronizedReentrantLock
实现方式JVM 内置,自动释放锁JDK 实现,需手动 lock()/unlock()
可中断不支持支持 lockInterruptibly()
公平锁非公平可配置公平/非公平
条件变量通过 wait()/notify()支持多个 Condition
性能Java 6 后优化,高竞争下略逊高竞争时表现更优

八、示例:双重检查锁定(DCL)

public class Singleton {
    private volatile static Singleton instance; // volatile 防止指令重排序
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 非原子操作需 volatile 保证可见性
                }
            }
        }
        return instance;
    }
}
  • 关键点
    volatile 防止指令重排序导致其他线程访问到未初始化的对象。

总结

synchronized 通过锁升级机制在大多数场景下提供了高效的同步支持,但其性能仍受锁竞争程度影响。开发者需结合具体场景选择同步策略,必要时使用工具(如 jstackJFR)监控锁状态,优化高并发程序的性能。