一、基础铺垫:synchronized 与 Java 对象的绑定关系

要理解 synchronized,首先要明确一个核心前提:Java 中每一个对象都隐式关联了一把 “对象锁”,而 synchronized 的本质就是对 “对象锁” 的操作。无论是 synchronized (obj) 修饰代码块,还是 synchronized 修饰实例方法(锁为 this)、静态方法(锁为类对象 Class),最终都是通过操作 “对象关联的锁机制” 实现同步。

而支撑这一机制的核心,是 Java 对象的内存结构—— 尤其是对象头中的 MarkWord(标记字段),它是锁状态的 “记录仪”,也是 synchronized 底层逻辑的关键载体。

二、对象内存结构:锁机制的 “物理基础”

每个 Java 对象在堆内存中存储时,都会分为 3 个部分,其中对象头是实现锁机制的核心:

内存区域作用说明
对象头(Header)存储对象元数据(锁状态、哈希码、GC 分代年龄等),是 synchronized 的核心依赖。
实例数据(Instance Data)存储对象的成员变量值(如类的字段),是线程安全问题的 “数据载体”。
对齐填充(Padding)JVM 要求对象内存大小为 8 字节的整数倍,不足时用此部分补齐,保证内存对齐。

重点:对象头的构成

对象头由两部分组成,其中 MarkWord 是锁机制的关键:

  1. 类型指针(Klass Pointer):指向对象对应的类元数据(如 User.class),用于确定对象的类型,JVM 通过它找到对象的类信息。
  2. MarkWord(标记字段):存储对象的锁状态、持有锁的线程 ID、指向 monitor 的指针等核心信息。其结构会随锁状态动态变化(无锁、偏向锁、轻量级锁、重量级锁),是锁升级的 “状态指示器”。

三、MarkWord:锁状态的 “动态记录仪”

MarkWord 是对象头中最核心的部分,其长度在 32 位 JVM 中为 4 字节(32 位),64 位 JVM 中为 8 字节(64 位)。它的结构会根据 “锁状态” 动态调整,以最大化利用内存空间。以下以 32 位 JVM 为例,展示不同锁状态下 MarkWord 的存储内容:

锁状态MarkWord 存储结构(32 位)核心作用
无锁状态哈希码(25 位) + 分代年龄(4 位) + 无锁标记(3 位)未被任何线程锁定,存储对象的哈希码(第一次调用 hashCode() 时计算)和 GC 分代年龄(用于 GC 回收判断)。
偏向锁状态线程 ID(23 位) + Epoch(2 位) + 分代年龄(4 位) + 偏向锁标记(3 位)记录 “偏向的线程 ID”,Epoch 用于批量撤销偏向锁(JVM 优化手段),避免频繁修改线程 ID。
轻量级锁状态锁记录指针(30 位) + 轻量级锁标记(2 位)指向线程栈帧中 “锁记录(Lock Record)” 的指针,通过 CAS 操作实现锁的获取与释放。
重量级锁状态monitor 指针(30 位) + 重量级锁标记(2 位)存储指向 monitor 对象的指针(ptr_to_heavyweight_monitor),关联重量级锁的核心同步机制。

关键结论MarkWord 是 synchronized 锁状态的 “载体”—— 通过修改 MarkWord 的结构,JVM 实现了 “锁状态切换” 和 “锁信息存储”,为后续的锁升级和同步操作提供基础。

四、monitor:重量级锁的 “同步核心”

当 synchronized 升级为重量级锁时,其底层依赖的是 monitor(管程)机制monitor 是一种操作系统级的同步原语,本质是一个 “对象监视器”,用于保证多线程对共享资源的互斥访问和线程协调。

1. monitor 的核心结构

每个 monitor 内部包含 3 个关键组件,共同实现线程的 “锁竞争” 和 “等待唤醒” 逻辑:

组件名称作用说明
Owner(持有线程)记录当前持有 monitor 的线程,同一时间仅能有一个线程成为 Owner(保证锁的独占性)。支持锁重入:同一线程多次获取锁时,会通过 “计数器” 记录重入次数(每次获取 +1,释放 -1,计数器为 0 时真正释放锁)。
EntryList(入口集)存储尝试获取 monitor 但失败的线程,这些线程处于 BLOCKED 状态,等待 Owner 释放锁后重新竞争。
WaitSet(等待集)存储调用 wait() 方法的线程:持有锁的线程调用 wait() 后,会释放 monitor 并进入 WaitSet,处于 WAITING/TIMED_WAITING 状态,需等待其他线程调用 notify()/notifyAll() 唤醒后,重新进入 EntryList 竞争锁。

2. monitor 与对象的关联方式

monitor 并非直接绑定对象,而是通过 MarkWord 间接关联:

  • 当锁升级为重量级锁时,JVM 会为对象分配一个 monitor 对象,并将 MarkWord 中的 “ptr_to_heavyweight_monitor” 字段设置为 monitor 的内存地址。
  • 后续线程对 “对象锁” 的操作(获取 / 释放),本质是通过 MarkWord 找到对应的 monitor,再对 monitor 的 OwnerEntryList 等组件进行操作。

3. 重量级锁的性能特点

monitor 涉及 用户态与内核态的切换:线程的阻塞(进入 EntryList)和唤醒(从 WaitSet 唤醒)需要操作系统介入,而上下文切换的成本较高。因此,重量级锁仅在 “多线程高频竞争” 场景下使用,JVM 会通过 “锁升级” 机制尽量避免直接使用重量级锁。

五、锁升级:synchronized 的 “性能优化核心”

早期的 synchronized 因直接使用重量级锁,性能较差。JDK 6 后,JVM 引入 “锁升级” 机制,通过无锁→偏向锁→轻量级锁→重量级锁的动态切换,在 “低竞争” 场景下减少同步开销,平衡性能与安全性。

锁升级的核心逻辑:根据线程竞争的激烈程度,动态选择成本最低的锁机制

1. 阶段 1:偏向锁(无实际竞争)

  • 适用场景:只有一个线程重复获取锁(无多线程竞争,如单线程循环加锁)。
  • 实现逻辑
    1. 线程第一次获取锁时,通过 CAS 操作将 MarkWord 中的 “线程 ID” 设置为当前线程 ID,并将锁标记设为 “偏向锁”。
    2. 后续该线程再次获取锁时,无需 CAS 操作,仅需对比 MarkWord 中的线程 ID 是否为自己
      • 是:直接重入锁(无任何开销)。
      • 否:说明出现线程竞争,需 “撤销偏向锁”(暂停持有偏向锁的线程,检查锁状态),并升级为轻量级锁。
  • 核心优势:几乎无同步开销,仅第一次加锁需 CAS,后续重入仅需对比线程 ID。

2. 阶段 2:轻量级锁(轻度竞争)

  • 适用场景:多个线程交替获取锁(无同时竞争,如线程 A 释放锁后,线程 B 再获取)。

  • 实现逻辑

    加锁过程:
    1. 线程在栈帧中创建一个 Lock Record(锁记录),存储两部分信息:
      • Object Reference:指向锁对象的引用。
      • Displaced MarkWord:复制锁对象 MarkWord 的当前值(作为解锁时的恢复依据)。
    2. 通过 CAS 操作将锁对象的 MarkWord 替换为 “指向当前 Lock Record 的指针”:
      • 成功:获取锁,将 MarkWord 的锁标记设为 “轻量级锁”。
      • 失败:说明有线程同时竞争锁(如线程 A 加锁时,线程 B 也在尝试加锁),轻量级锁升级为重量级锁。
    解锁过程:
    1. 通过 CAS 操作将锁对象的 MarkWord 恢复为 Displaced MarkWord(即无锁状态的 MarkWord):
      • 成功:释放锁,无竞争。
      • 失败:说明锁已升级为重量级锁,需调用 monitorexit 指令释放 monitor,并唤醒 EntryList 中的线程。
  • 核心优势:通过 CAS 操作避免 monitor 的内核态切换,适合轻度竞争场景。

3. 阶段 3:重量级锁(重度竞争)

  • 适用场景:多个线程同时竞争锁(高频并发,如线程 A 未释放锁时,线程 B、C 同时尝试获取)。
  • 实现逻辑
    1. 当轻量级锁的 CAS 操作失败(存在同时竞争)时,JVM 会将锁升级为重量级锁:
      • 将锁对象 MarkWord 中的 “ptr_to_heavyweight_monitor” 设为 monitor 的地址。
      • 未获取锁的线程进入 monitor 的 EntryList,处于 BLOCKED 状态。
    2. 持有锁的线程释放锁时,会调用 monitorexit 指令:
      • 将 monitor 的 Owner 设为 null,计数器清零。
      • 唤醒 EntryList 中的线程,重新竞争 monitor
  • 核心劣势:涉及用户态与内核态切换,上下文切换成本高,但能保证多线程并发安全。

六、synchronized 的核心特性:原子性、可见性、有序性

synchronized 之所以能解决线程安全问题,是因为它同时保证了原子性、可见性、有序性三大核心特性。这三大特性的实现,依赖于前文提到的 “锁机制” 和 “内存屏障”。

1. 原子性保证:操作不可分割

原子性定义:一个操作或一系列操作,要么全部执行完成,要么全部不执行,中间不会被其他线程打断。

实现原理:
  • 偏向锁 / 轻量级锁:通过 CAS 操作的原子性 实现。
    • 偏向锁的线程 ID 设置、轻量级锁的 MarkWord 替换,均通过 CPU 的 CAS 指令(不可分割)保证操作不被打断。
    • 同步块内的操作因锁的独占性(同一时间仅一个线程执行),避免多线程交叉干扰。
  • 重量级锁:通过 monitor 的独占性 实现。
    • 同一时间仅一个线程能成为 monitor 的 Owner,同步块内的操作作为 “整体” 执行,不会被其他线程打断。
示例:count++ 的原子性

count++ 本质是 “读取 count → 加 1 → 写入 count” 三步操作,不加锁时会因线程交叉执行导致错误。而 synchronized 通过锁的独占性,将这三步操作 “打包” 为原子单元,确保多线程下 count 的正确性。

2. 可见性保证:修改可被感知

可见性定义:当一个线程修改了共享变量的值后,其他线程能 “立即” 看到这个修改后的结果。

实现原理:依赖 “锁操作 + 内存屏障”

JVM 会在 synchronized 的 “获取锁” 和 “释放锁” 阶段,自动插入 内存屏障(Memory Barrier)—— 一组禁止指令重排序、强制缓存同步的 CPU 指令,确保共享变量的修改被其他线程感知。

锁操作插入的内存屏障作用说明
释放锁(退出同步块)StoreStore 屏障 + StoreLoad 屏障1. StoreStore 屏障:确保释放锁前,线程对共享变量的所有写入操作(Store)已刷新到主内存,防止后续写入重排序到释放锁前。2. StoreLoad 屏障:确保主内存已接收所有写入,防止释放锁后的读取操作(Load)重排序到释放锁前,避免其他线程读到旧值。
获取锁(进入同步块)LoadLoad 屏障 + LoadStore 屏障1. LoadLoad 屏障:确保线程从主内存加载共享变量的最新值(Load),清空本地内存的旧值,防止后续读取使用缓存旧值。2. LoadStore 屏障:确保读取操作完成后,再执行后续写入操作,避免写入使用未更新的旧值。

核心逻辑:释放锁时强制刷新本地内存到主内存,获取锁时强制从主内存加载最新值,确保多线程对共享变量的修改可被感知。

3. 有序性保证:避免指令重排序

有序性定义:程序执行的顺序与代码的逻辑顺序一致,避免 CPU 为优化性能而进行的 “指令重排序” 导致的执行混乱。

实现原理:
  • 直接保证:通过 “锁的独占性”—— 同一时间仅一个线程执行同步块内的代码,确保同步块内的操作按逻辑顺序执行,不会与其他线程的操作交叉。
  • 间接保证:通过 “内存屏障”——StoreStoreStoreLoadLoadLoadLoadStore 屏障禁止了指令重排序,确保同步块内的指令按代码顺序执行,且同步块外的指令不会与同步块内的指令重排序。

注意synchronized 仅保证 “同步块内的操作相对有序”,不保证 “同步块外的操作有序”—— 这与 volatile 的 “禁止指令重排序” 范围不同,但已能满足多线程安全的需求。

七、扩展:synchronized 与 volatile 的对比

很多人会混淆 synchronized 和 volatile,两者虽都用于解决线程安全问题,但定位和能力不同,对比如下:

特性synchronizedvolatile
原子性支持(同步块内操作原子)不支持(仅保证单个变量的读写可见,不保证复合操作原子)
可见性支持(通过内存屏障强制缓存同步)支持(通过内存屏障禁止缓存,强制读写主内存)
有序性支持(通过锁独占性和内存屏障避免重排序)支持(通过内存屏障禁止指令重排序)
锁机制支持偏向锁、轻量级锁、重量级锁(动态升级)无锁机制(仅内存语义)
适用场景复合操作(如 count++、多步业务逻辑)单个变量的读写(如状态标记 flag

八、总结

synchronized 并非简单的 “锁”,而是一套从 “对象结构” 到 “锁机制”,再到 “内存语义” 的完整体系:

  1. 基础载体:Java 对象的 MarkWord 存储锁状态,是锁升级的 “动态记录仪”。
  2. 核心机制:通过 “偏向锁→轻量级锁→重量级锁” 的升级流程,平衡性能与并发安全,避免不必要的内核态切换。
  3. 同步核心:重量级锁依赖 monitor 的独占性,实现线程的互斥访问和等待唤醒。
  4. 特性保证:原子性靠 “锁的独占性” 或 “CAS 原子性”,可见性和有序性靠 “锁操作时插入的内存屏障”。

理解 synchronized 的底层原理,不仅能帮助你在实际开发中 “合理使用锁”(如避免过度同步导致性能问题),更能让你深入理解 JVM 对多线程的优化思路 ——用最低的成本解决最复杂的并发问题

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐