本文深入 synchronized 的底层原理,主要关于 JMM、内存屏障、happens-before 的讨论。


一、synchronized 提供了什么?

synchronized 是 Java 内置的重量级锁(但 JDK 1.6 后做了锁升级优化,下文会说),它保证三个特性:

特性 是否保证 说明
原子性 被 synchronized 包裹的代码块(或方法)在同一时刻只能有一个线程执行
可见性 锁释放时会强制将工作内存中的修改刷新到主内存;锁获取时会强制重新从主内存加载最新值
有序性 锁内的代码不会被重排序到锁外(但仍可在锁内重排序,不影响可见性)

二、synchronized 的 JMM 语义——happens-before 规则

JMM 定义了锁规则

  • 解锁(unlock)操作 happens-before 后续对同一个锁的加锁(lock)操作

结合程序顺序规则和传递性,可以推出:

  • 线程 A 在锁内写操作 happens-before 线程 A 释放锁;
  • 线程 A 释放锁 happens-before 线程 B 获得同一个锁;
  • 因此线程 A 锁内的所有写操作对线程 B 锁内的读操作可见。

这与 volatile 的写-读规则本质上是一致的,只是 synchronized 可以包围多个操作,而 volatile 只针对单个变量。


三、底层实现:内存屏障与锁指令

synchronized 在 JVM 层面的实现依赖 monitor 机制(每个对象关联一个 monitor),字节码层面有 monitorenter / monitorexit 指令。在底层(x86 为例)则涉及 lock 前缀的指令和 MESI 协议。

3.1 获取锁时的内存语义

当线程获取锁时(进入 synchronized 块),JVM 会插入一个 LoadLoadLoadStore 屏障(类似 volatile 读的效果),以保证:

  • 从主内存重新加载共享变量,清除工作内存中的旧值。
  • 后续的读/写操作不会被重排到获取锁之前。

实际上,获取锁的动作本身会触发缓存一致性操作,保证进入临界区前看到其他线程释放锁前的最新值。

3.2 释放锁时的内存语义

当线程释放锁时(退出 synchronized 块),JVM 会插入一个 StoreStoreStoreLoad 屏障(类似 volatile 写的效果),以保证:

  • 临界区内的所有写操作先于锁释放而被刷入主内存。
  • 锁释放后的任何操作(例如后续的普通读)不会重排到锁释放之前。

在 x86 上,monitorexit 通常对应 lock 前缀的指令(例如 lock cmpxchglock addl $0, (%rsp)),实现全屏障效果。


四、对象头与锁的状态(锁升级)

Java 对象的内存布局包含对象头(Mark Word),用于存储锁信息。JDK 1.6 以后引入了锁升级机制,从无锁到偏向锁、轻量级锁、重量级锁,以优化性能。

锁状态 适用场景 Mark Word 内容 获取/释放开销
无锁 对象未被锁定 哈希码、分代年龄 -
偏向锁 单线程反复进入同步块 线程 ID(偏向的线程) 极低(仅一次 CAS)
轻量级锁 多线程交替进入,无竞争 指向线程栈中 Lock Record 的指针 CAS 操作(自旋)
重量级锁 竞争激烈 指向 monitor 对象的指针 操作系统互斥量(用户态→内核态)

锁升级过程(不可逆,除了偏向锁可撤销):

  1. 初始无锁 → 偏向锁(如果 JVM 启动偏向锁延迟后,第一个线程进入时设置偏向线程 ID)。
  2. 偏向锁遇到竞争(另一线程请求同一对象的锁)→ 撤销偏向锁 → 升级为轻量级锁。
  3. 轻量级锁自旋失败(或者自旋次数超过阈值)→ 膨胀为重量级锁。

无论哪种锁,最终都需要保证 JMM 的内存语义(可见性、有序性)。


五、synchronizedvolatile 的对比总结

特性 synchronized volatile
原子性 保证代码块内操作原子 不保证(仅单次读写原子)
可见性 释放锁时刷新内存;获取锁时重载 写后立即刷新;读前重新加载
有序性 锁内代码不会与锁外重排 禁止特定重排序
使用粒度 方法或代码块(可多个变量) 单个变量(字段)
是否阻塞 可能阻塞(重量级锁时) 不阻塞,无上下文切换
性能 有锁升级优化,但竞争时仍较重 非常轻量(仅加入内存屏障)
适用场景 复合操作、需要原子性 状态标志、一次性安全发布(如 DCL)

六、典型错误:synchronized 不能保证不可变对象的可见性?

有人误以为 synchronized 不能保证 final 字段的可见性,实际上只要正确使用了 synchronized(获取/释放同一个锁),其内存屏障和 happens-before 规则会使得所有字段(无论是否 final)的更新对其他线程可见。final 的特殊性在于不依赖同步也能安全发布,但 synchronized 依然是全功能的保证。


七、代码示例:synchronized 的内存效果

public class SynchVisibility {
    private int x = 0;
    private boolean ready = false;

    public synchronized void writer() {
        x = 42;      // 写普通变量
        ready = true; // 写普通变量
        // 释放锁时:StoreStore + StoreLoad 屏障
    }

    public synchronized void reader() {
        // 获取锁时:LoadLoad + LoadStore 屏障(重新从主存加载)
        if (ready) {
            System.out.println(x); // 保证看到 42
        }
    }
}

如果两个线程分别调用 writer()reader()(使用同一对象实例),则 reader() 看到 ready==true 时,必然看到 x=42。这里没有用 volatilefinal,全靠 synchronized 的内存语义保证。


八、底层实现的简化模型(JVM 抽象)

JVM 在实现 synchronized 时,可以认为在 monitorentermonitorexit 前后插入了类似于下列的屏障:

monitorenter
[LoadLoad]  // 保证从内存读取最新值,且不会与下面的读重排
[LoadStore] // 保证读取不会与下面的写重排

// 临界区代码(可能包含读、写操作)

[StoreStore] // 保证临界区内的写不会与释放锁重排
[StoreLoad]  // 保证释放锁时所有写已刷入主存,且后续读不会重排到释放锁之前
monitorexit

不同 CPU 架构对这些屏障的实现不同(x86 会折叠许多屏障为 lock 前缀指令,ARM 需要显式 dmb 等)。


九、与 volatile 的性能比较

  • volatile 只影响单个变量,且没有锁竞争,开销恒定且很小。
  • synchronized 在无竞争时(偏向锁或轻量级锁)开销也很低,但一旦有竞争就可能进入重量级锁,涉及操作系统内核态切换,开销变大。
  • 因此,尽量选择 volatile 如果满足需求;否则使用 synchronized(或 Lock)。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐