在 Java 并发编程中,synchronized 是解决线程安全问题最基础、最核心的关键字。很多开发者会用它,但90% 的人都没真正理解它的底层是如何工作的:
- 为什么加了
synchronized就能保证线程互斥? - 锁信息存在哪里?对象头又是什么?
monitorenter/monitorexit到底做了什么?- JDK 1.6 之后的锁升级(偏向锁→轻量级锁→重量级锁)原理是什么?
这篇博客将从 JVM 底层、字节码、对象结构、Monitor 机制、锁升级 五个维度,彻底讲透 synchronized 的底层实现,保证通俗易懂、逻辑清晰。
一、前置知识:Java 对象内存布局
要理解 synchronized,必须先知道:Java 的锁,是存在对象头里的。
一个 Java 对象在内存中分为三部分:
- 对象头(Header) ------ 存储锁信息
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头核心结构(关键)
对象头里最重要的是:Mark Word它存储了:
- 对象的 HashCode
- GC 分代年龄
- 锁状态标志位
- 持有锁的线程 ID
- 指向 Monitor 的指针
Mark Word 是 synchronized 实现锁机制的物理基础。
二、synchronized 底层字节码实现
我们先看最直观的:编译后的字节码。
写一段最简单的同步代码块:
java
public class SyncDemo {
private final Object lock = new Object();
public void test() {
synchronized (lock) {
// 临界区
}
}
}
使用 javap -c 查看字节码:
php
0: aload_0
1: getfield
4: monitorenter # 获取锁
5: ... # 执行代码
18: monitorexit # 正常释放锁
19: goto 27
22: monitorexit # 异常释放锁
核心结论
-
monitorenter线程进入同步代码块时执行,尝试获取对象的 Monitor 锁。 -
monitorexit线程退出同步代码块时执行,释放 Monitor 锁。 -
为什么有两个 monitorexit?
- 一个用于正常退出
- 一个用于异常退出JVM 保证锁一定会被释放,不会锁泄漏
这就是 synchronized 自动释放锁的底层原因。
三、Monitor 机制:synchronized 的核心引擎
什么是 Monitor?
Monitor = 监视器锁 它是 JVM 内部的一个同步工具,本质是一个同步队列结构。
每个对象都会关联一个 Monitor。
Monitor 内部结构
php
Monitor {
int owner // 持有锁的线程
int count // 重入次数
EntryList // 阻塞等待锁的线程
WaitSet // 调用 wait() 后的线程
}
执行流程(最重要)
- 线程执行
monitorenter - 检查
owner是否为空- 为空 → 获取锁,
owner=当前线程,count=1 - 不为空 → 检查是不是当前线程(可重入 )
- 是 → count++
- 否 → 进入
EntryList阻塞
- 为空 → 获取锁,
- 线程执行
monitorexit- count--
- count=0 → 释放锁,唤醒 EntryList 线程
一句话总结
synchronized 的互斥性,完全由 Monitor 实现。
四、synchronized 锁升级机制(JDK 1.6 优化)
JDK 1.6 以前,synchronized 是重量级锁 (效率低)。JDK 1.6 引入锁升级,让锁根据竞争程度自动升级:
锁升级流程(固定顺序)
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
1. 偏向锁(Biased Lock)
适用场景:只有一个线程反复获取锁
原理
- 锁会偏向第一个获取它的线程
- Mark Word 存储线程 ID
- 下次该线程再获取锁,不需要 CAS,不需要竞争
- 直接判断线程 ID 一致就通过
优点
几乎无锁开销,单线程下效率极高。
撤销
当第二个线程尝试竞争锁时,偏向锁立即撤销。
2. 轻量级锁(Lightweight Lock)
适用场景:两个线程交替执行,竞争不激烈
原理
- 线程在自己栈帧中创建锁记录(Lock Record)
- 用 CAS 操作 尝试将对象 Mark Word 指向锁记录
- CAS 成功 → 获取锁
- CAS 失败 → 自旋(spin) 重试
优点
- 不进入内核态
- 不阻塞线程
- 竞争低时比重量级锁快很多
升级
自旋一定次数仍失败 → 升级为重量级锁
3. 重量级锁(Heavyweight Lock)
适用场景:多线程激烈竞争
原理
- 依赖 Monitor
- 未获取锁的线程直接进入 EntryList 阻塞
- 涉及 用户态 ↔ 内核态切换,开销大
特点
- 最稳定
- 开销最大
- 高竞争下唯一可靠的锁
锁升级总结
| 锁类型 | 实现机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 偏向锁 | 线程 ID 标记 | 无开销 | 多线程竞争会撤销 | 单线程 |
| 轻量级锁 | CAS + 自旋 | 无阻塞 | 自旋消耗 CPU | 交替执行 |
| 重量级锁 | Monitor | 稳定 | 内核切换、阻塞 | 高并发竞争 |
五、synchronized 如何保证原子性、可见性、有序性
1. 原子性
通过 Monitor 互斥 实现,同一时刻只有一个线程进入临界区。
2. 可见性
解锁前必须将修改刷新回主内存 加锁前必须从主内存重新读取
3. 有序性
- 同步块内具有 happens-before 语义
- 禁止指令重排
六、synchronized 的可重入性原理
synchronized 是可重入锁,即同一个线程可以反复获取同一把锁。
底层原理
Monitor 中有一个 count 字段:
- 加锁:count++
- 释放:count--
- count=0 才真正释放锁
所以同一个线程再次进入同步块不会被自己阻塞。
七、synchronized 与 Lock 的对比(拓展)
在 Java 并发编程中,除了 synchronized,还有 Lock(如 ReentrantLock)也是常用的锁机制。很多开发者会疑惑:两者该如何选择?下面从多个维度对比,帮你快速区分。
| 对比维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 锁的实现 | JVM 层面,内置锁,自动释放 | API 层面,手动实现,需手动释放(try-finally) |
| 锁的释放 | 自动释放(正常执行完毕 / 异常) | 手动释放(必须在 finally 中释放,避免锁泄露) |
| 锁的类型 | 非公平锁(默认),可通过 JVM 参数设置为公平锁 | 可设置公平锁 / 非公平锁(构造方法参数) |
| 锁的功能 | 基础锁功能,支持重入 | 支持重入、中断、超时、条件变量等高级功能 |
| 性能 | JDK1.6 优化后,性能接近 Lock,简单场景更优 | 复杂场景(如超时、中断)性能更优,灵活性更高 |
| 适用场景 | 简单并发场景,无需高级功能 | 复杂并发场景(如超时获取锁、中断锁、多条件等待) |
总结
简单场景(如单线程访问、少量线程竞争)用 synchronized(简单、无需手动释放);复杂场景用 Lock(灵活、支持高级功能)。
八、总结:吃透 synchronized 核心逻辑
synchronized 是 Java 并发编程的基石,底层原理可以浓缩为以下几点:
-
核心作用 通过互斥,保证代码的原子性、可见性、有序性,解决线程安全问题。
-
使用方式
- 修饰实例方法(锁 this)
- 修饰静态方法(锁 Class)
- 修饰代码块(锁任意对象)
-
底层实现
- 依赖 Monitor 实现互斥
- 依赖 对象头 Mark Word 存储锁状态
- 通过
monitorenter/monitorexit指令获取 / 释放锁
-
锁升级机制无锁 → 偏向锁 → 轻量级锁 → 重量级锁根据竞争程度自动升级,大幅提升效率。
-
实战避坑
- 锁对象必须用
final修饰 - 尽量使用同步代码块,缩小锁粒度
- 不要混淆实例锁和类锁
- 避免多线程交叉获取锁造成死锁
- 锁对象必须用