synchronized 是怎么实现的?

synchronized 的实现分为 语法层面 和 运行时层面。

  1. 语法层面:它通过 monitorenter 和 monitorexit 这一对字节码指令来实现同步代码块的进入和退出;对于同步方法,则通过方法常量池中的 ACC_SYNCHRONIZED 标志位来标识。
  2. 运行时层面(核心):JVM 将每个 Java 对象都关联一个 监视器锁(Monitor)。线程通过竞争对象头中的 Mark Word 来获取这个 Monitor。在 JDK 6 之后,为了减少性能开销,引入了 锁升级机制:锁状态会根据竞争情况,从 无锁 -> 偏向锁 -> 轻量级锁(自旋锁) -> 重量级锁 逐步升级。

深度解析

原理/机制

1. 对象头与 Monitor

  • 每个 Java 对象在内存中分为三部分:对象头、实例数据、对齐填充。
  • 对象头 中的 Mark Word(标记字)是实现锁的关键。它存储了对象的哈希码、分代年龄和 锁状态标志。
  • 当锁升级为重量级锁时,Mark Word 中会存储一个指向 操作系统层级互斥量(mutex) 和 等待队列 的指针,这个结构就是 ObjectMonitor(即 Monitor 的具体实现)。

2. 锁升级过程(JDK 6+ 优化后) 这是理解现代 synchronized 性能的关键。

  • 偏向锁:假设锁总是由同一线程获得。当一个线程首次进入同步块时,会在对象头和栈帧锁记录中存储偏向的线程 ID。以后该线程再进入时,只需简单检查 ID,无需任何原子操作。适用于近乎无竞争的单线程场景。
  • 轻量级锁:当有第二个线程尝试获取锁(发生轻微竞争),偏向锁会升级为轻量级锁。线程会在自己的栈帧中创建锁记录(Lock Record),并尝试通过 CAS 操作 将对象头的 Mark Word 复制到自己的锁记录中,并替换为指向锁记录的指针。如果成功,则获取锁;如果失败,说明存在竞争,会 自旋(循环尝试)一定次数。
  • 重量级锁:如果轻量级锁自旋失败(或自旋超过阈值),锁会膨胀为重量级锁。此时,未竞争到锁的线程会被 挂起(Park),进入阻塞状态,等待操作系统调度唤醒。这涉及到用户态到内核态的切换,开销最大。

代码示例

从字节码视角看一个同步块:

// Java 源码
public void syncMethod() {
    synchronized(this) {
        System.out.println("hello");
    }
}

编译后的关键字节码指令如下(可通过 javap -c 查看):

public void syncMethod();
  Code:
     0: aload_0         // 将对象引用(this)压入操作数栈
     1: dup             // 复制栈顶值
     2: astore_1        // 存储一个副本到局部变量表(用于后续 monitorexit)
     5: monitorenter    // 尝试获取对象的 Monitor
     6: getstatic     #2 // 开始执行同步块内的代码
     9: ldc           #3
    11: invokevirtual #4
    14: aload_1
    15: monitorexit     // 正常退出,释放 Monitor
    16: goto          24
    19: astore_2        // 异常处理开始...
    20: aload_1
    21: monitorexit     // 异常退出,也必须释放 Monitor
    22: aload_2
    23: athrow
    24: return

可以看到,编译器会自动生成配对且确保一定执行的 monitorenter 和 monitorexit 指令,这正是 synchronized 能在发生异常时也释放锁的原因。

对比分析:synchronized vs ReentrantLock

特性synchronized (隐式锁)ReentrantLock (显式锁)
实现层面 JVM 层面实现,由 C++ 代码控制 JDK 层面实现,Java 代码(基于 AQS)
锁的获取 自动加锁与释放锁 必须手动 lock() 和 unlock(),通常配合 try-finally
灵活性 有限。不可中断、非公平(可设公平但不灵活) 很高。可尝试非阻塞获取(tryLock)、可中断、可设置公平/非公平
条件队列 单一等待队列 (wait/notify) 可绑定多个 Condition 对象,实现精细的线程等待/唤醒
性能 JDK 6 后优化出色,在一般竞争下与 ReentrantLock 持平 在高竞争场景下,其可配置性可能带来优势

最佳实践与注意事项

  • 锁对象选择:应选择 不可变且所有竞争线程都可见 的对象作为锁。通常使用私有静态 final 对象或 this(但需谨慎)。
  • 减小锁粒度:尽量只锁必要的代码段(同步块优于同步方法),缩短锁的持有时间。
  • 避免死锁:按固定顺序获取多个锁。
  • 现代 Java 开发:在竞争不激烈的常规业务代码中,优先使用简洁的 synchronized。只有在需要其不具备的高级特性(如可中断、超时、公平锁、多个条件变量)时,才使用 ReentrantLock

常见误区

  • 误区一:synchronized 性能一定很差。纠正:在 JDK 6 引入锁升级后,它在无竞争或低竞争场景下开销极小,性能很好。
  • 误区二:锁升级是双向的。纠正:锁升级(膨胀)是 单向 的,一旦升级为重量级锁,就不会再降级回轻量级锁(在某些 JVM 实现中,重量级锁释放后可回到无锁状态,但不会回到偏向/轻量级状态)。
  • 误区三:synchronized 锁的是代码。纠正:它锁的是 对象,确切地说是对象的 Monitor。
posted @ 2026-03-24 09:13  DBA日记  阅读(4)  评论(0)    收藏  举报