synchronized 是 Java 内置的悲观锁 ,核心作用是保证多线程环境下的原子性、可见性和有序性,从 JDK 1.6 开始进行了大量优化(如偏向锁、轻量级锁),性能大幅提升,成为并发编程中最常用的同步手段之一。
一、底层核心原理
synchronized 的实现依赖 Java 对象头(Mark Word) 和 监视器锁(Monitor) ,其核心逻辑是:通过竞争 Monitor 的所有权实现线程互斥。
1. 核心依赖:监视器锁(Monitor)
Monitor 是 JVM 层面的抽象概念,本质是一种同步工具,可理解为 "锁的持有者管理机制",每个 Java 对象都隐式关联一个 Monitor(即 "对象锁" 的底层载体)。
Monitor 的结构(简化):
- Owner:当前持有锁的线程(同一时间只能有一个线程持有)。
- EntryList:等待获取锁的线程队列(线程处于 BLOCKED 状态)。
- WaitSet :调用
wait()后释放锁的线程队列(线程处于 WAITING 状态)。
Monitor 的核心逻辑:
-
线程尝试获取锁时,会竞争 Monitor 的 Owner 位置:
- 若 Owner 为空,当前线程直接成为 Owner,持有锁。
- 若 Owner 已被其他线程占用,当前线程进入 EntryList 阻塞。
-
线程释放锁时(退出同步块 / 方法、调用
wait()):- 若调用
wait(),线程释放 Owner 身份,进入 WaitSet 等待被唤醒。 - 若正常释放,Owner 置空,JVM 从 EntryList 唤醒一个线程竞争锁。
- 若调用
2. 锁状态的存储基础:Java 对象头(Mark Word)
Java 对象在内存中分为 3 部分:对象头、实例数据、对齐填充 。其中,对象头的 Mark Word 是存储锁状态的关键。
Mark Word 的结构(动态变化,32 位 JVM 示例):
| 锁状态 | Mark Word 存储内容 |
|---|---|
| 无锁 | HashCode(25 位) + 对象年龄(4 位) + 是否偏向锁(1 位,0) + 锁状态(2 位,01) |
| 偏向锁 | 偏向线程 ID(23 位) + Epoch(2 位) + 对象年龄(4 位) + 是否偏向锁(1 位,1) + 锁状态(2 位,01) |
| 轻量级锁 | 指向栈帧中 "锁记录(Lock Record)" 的指针(30 位) + 锁状态(2 位,00) |
| 重量级锁 | 指向 Monitor 的指针(30 位) + 锁状态(2 位,11) |
- 锁状态由 2 位标志位 + 是否偏向锁标志位 共同决定。
- Mark Word 的动态变化是
synchronized锁升级的核心依据。
3. synchronized 的两种使用方式及底层实现
synchronized 可修饰方法 或代码块,底层实现略有差异,但核心都是竞争 Monitor。
(1)修饰代码块:monitorenter + monitorexit 指令
编译后,同步代码块的前后会插入 monitorenter 和 monitorexit 字节码指令:
- monitorenter:线程进入时执行,尝试获取 Monitor 所有权(成功则 Owner 设为当前线程,失败则阻塞)。
- monitorexit:线程退出时执行,释放 Monitor 所有权(Owner 置空,唤醒等待线程)。
注意:编译器会生成 2 个 monitorexit:一个对应正常退出,一个对应异常退出(确保锁一定释放,避免死锁)。
(2)修饰方法:ACC_SYNCHRONIZED 标志
修饰方法时,字节码中不会插入指令,而是在方法表(method_info) 中添加 ACC_SYNCHRONIZED 标志:
- 线程调用方法时,JVM 检查该标志:若存在,先获取 Monitor 锁,再执行方法体。
- 方法执行完毕(正常返回 / 抛出异常),JVM 自动释放 Monitor 锁。
类锁 vs 对象锁
- 对象锁 :修饰实例方法或代码块(锁对象为
this或自定义对象),竞争的是 "实例对象关联的 Monitor"。 - 类锁 :修饰静态方法或代码块(锁对象为
XXX.class),竞争的是 "类对象(Class 实例)关联的 Monitor"。 - 本质:类锁也是对象锁(Class 是 JVM 加载的单例对象),两者独立,互不干扰。
二、JDK 1.6+ 核心优化机制
早期 synchronized 是 "重量级锁",线程竞争失败会直接阻塞(切换到内核态,开销大)。JDK 1.6 引入锁升级机制 和其他优化,让 synchronized 在无竞争 / 轻度竞争场景下性能接近乐观锁(如 ReentrantLock)。
核心优化思路:根据竞争强度动态切换锁状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),减少无竞争 / 轻度竞争时的开销。
1. 锁升级机制(核心优化)
锁升级是不可逆的(只能从低开销向高开销升级),目的是 "按需分配资源":无竞争时用偏向锁,轻度竞争时用轻量级锁,激烈竞争时用重量级锁。
(1)偏向锁:无竞争场景的最优解
适用场景 :锁由同一线程多次获取,无其他线程竞争(如单线程循环调用同步方法)。核心思想:"偏向" 第一个获取锁的线程,后续该线程无需竞争,直接持有锁。
实现原理:
-
线程第一次获取锁时,通过 CAS 将 Mark Word 中的 "偏向线程 ID" 设为当前线程 ID,"是否偏向锁" 设为 1,锁状态保持 01(偏向锁标识)。
-
后续该线程再次获取锁时,仅需检查:
- Mark Word 中的偏向线程 ID 是否为当前线程。
- 偏向锁标志是否为 1。
- 锁状态是否为 01。满足则直接获取锁,无需 CAS 或阻塞,开销极低。
偏向锁的撤销:
当有其他线程尝试竞争偏向锁时,偏向锁会被 "撤销",升级为轻量级锁。撤销流程:
-
暂停持有偏向锁的线程(STW 短暂停顿)。
-
检查持有线程的状态:
- 若线程已终止,直接将 Mark Word 重置为无锁状态。
- 若线程仍存活,将锁升级为轻量级锁,持有线程继续执行,竞争线程进入轻量级锁竞争。
(2)轻量级锁:轻度竞争场景的优化
适用场景 :少量线程竞争锁,且竞争时间短(如两个线程交替获取锁)。核心思想:用 "自旋" 替代 "阻塞",避免线程切换到内核态的开销。
实现原理:
-
线程获取锁时,先在当前栈帧中创建一个 Lock Record(锁记录) ,存储对象当前的 Mark Word 副本(Displaced Mark Word)。
-
通过 CAS 将对象的 Mark Word 更新为 "指向当前 Lock Record 的指针":
- CAS 成功:线程获取轻量级锁,锁状态变为 00。
- CAS 失败:说明有其他线程竞争,此时会先自旋(空循环)几次,尝试再次获取锁。
轻量级锁的膨胀:
若自旋失败(如自旋次数耗尽仍未获取锁,或有更多线程参与竞争),轻量级锁会 "膨胀" 为重量级锁:
- 将 Mark Word 中的指针改为 "指向 Monitor 的指针",锁状态变为 11。
- 未获取锁的线程进入 EntryList 阻塞(BLOCKED 状态),避免无效自旋浪费 CPU。
(3)重量级锁:激烈竞争场景的兜底
适用场景 :多个线程同时竞争锁,且竞争时间长(如线程持有锁执行耗时操作)。实现原理 :依赖操作系统的 互斥量(Mutex) 实现,线程竞争失败会直接阻塞(从用户态切换到内核态),等待被唤醒。特点:开销最大(线程阻塞 / 唤醒需内核态切换),但稳定性最高,适合激烈竞争场景。
2. 其他关键优化
(1)自旋锁与适应性自旋锁
-
自旋锁:轻量级锁竞争时,线程不直接阻塞,而是自旋(空循环)一段时间(默认 10 次),等待持有锁的线程快速释放。自旋无需切换内核态,适合锁持有时间短的场景。
-
适应性自旋锁:JDK 1.6 优化,自旋次数不再固定,而是根据 "历史自旋成功率" 动态调整:
- 若之前自旋成功获取锁,下次自旋次数增加(如 20 次)。
- 若之前自旋失败,下次自旋次数减少或直接放弃自旋(避免浪费 CPU)。
(2)锁消除
JIT 编译器在编译时,通过逃逸分析判断:若一个锁对象仅被当前线程访问(无逃逸到其他线程),则直接消除该锁。
- 示例:
StringBuffer的append()方法是同步方法,但单线程环境下,JIT 会消除其锁(因为StringBuffer对象未逃逸,无需同步)。
(3)锁粗化
若多个连续的锁操作针对同一个对象,JIT 会将这些分散的锁合并为一个 "粗粒度锁",减少锁的获取 / 释放次数。
- 示例:循环中多次调用
synchronized (this) { ... },JIT 会将锁粗化到循环外部,仅获取一次锁即可。
三、synchronized 如何保证可见性,有序性,原子性
synchronized 是 Java 中唯一能同时保证 原子性、可见性、有序性 的内置同步机制,其保障逻辑完全基于底层的 Monitor 监视器锁 和 JMM(Java 内存模型)规则,与之前提到的底层原理(如锁获取 / 释放、对象头操作)深度绑定。下面分三个特性,拆解其具体保障机制:
1、保证原子性:基于 Monitor 的互斥执行
1.1. 原子性的定义
原子性指 一个操作或一组操作,要么全部执行且执行过程不被打断,要么全部不执行 (不可分割)。比如 i++ 本质是「读取 i → 加 1 → 写入 i」三个步骤,若不加同步,多线程环境下可能被其他线程打断,导致结果错误。
1.2. synchronized 如何保证原子性?
核心逻辑:通过 Monitor 锁的互斥性,确保同步块 / 方法在同一时间只能被一个线程执行。
结合底层原理的细节:
- 线程要进入同步块 / 方法,必须先获取 Monitor 的所有权(通过
monitorenter指令或ACC_SYNCHRONIZED标志)。 - Monitor 的
Owner字段同一时间只能指向一个线程(互斥性),其他竞争线程会被阻塞在EntryList(BLOCKED 状态),直到当前线程释放锁。 - 同步块 / 方法内的所有操作,会作为一个 "整体" 被执行 ------ 在当前线程释放锁前,其他线程无法插入执行,因此这组操作具备不可分割的原子性。
示例验证:
arduino
private int count = 0;
private synchronized void increment() {
count++; // 读取、加1、写入三个步骤,被synchronized保证为原子操作
}
- 若不加
synchronized,1000 个线程各调用 1000 次increment(),最终count会小于 1000000(因步骤被打断)。 - 加
synchronized后,count++的三个步骤被 "串行化",最终结果必然是 1000000,原子性得到保证。
补充:可重入性不破坏原子性
synchronized 是可重入锁(同一线程可多次获取同一锁),但多次获取不会打破互斥性 ------ 其他线程仍需等待当前线程完全释放锁(所有重入的锁都释放)才能竞争,因此原子性依然成立。
2、保证可见性:基于锁释放 / 获取的内存刷新规则
2.1. 可见性的定义
可见性指 一个线程修改了共享变量的值后,其他线程能立刻感知到这个修改。若没有可见性保障,线程 A 修改的变量可能只存在于自己的工作内存中,未同步到主内存,线程 B 读取的仍是主内存中旧值。
2.2. synchronized 如何保证可见性?
核心逻辑:JMM 为 synchronized 定义了「锁释放 - 获取的内存语义」,强制刷新共享变量的内存。
具体规则(与底层锁操作绑定):
- 锁释放时:强制刷新工作内存到主内存 线程释放 Monitor 锁时(执行
monitorexit指令,或方法执行完毕),JVM 会触发一个动作:将该线程在工作内存中修改的所有共享变量,强制刷新到主内存(清空工作内存中的缓存,确保主内存是最新值)。 - 锁获取时:强制从主内存加载最新值 线程获取 Monitor 锁时(执行
monitorenter指令,或调用同步方法),JVM 会触发一个动作:将该线程工作内存中对应的共享变量 置为无效,后续读取该变量时,必须从主内存重新加载(确保拿到的是最新值)。
示例验证:
arduino
private boolean flag = false;
private synchronized void setFlag() {
flag = true; // 线程A修改后,释放锁时刷新到主内存
}
private synchronized void checkFlag() {
if (flag) { // 线程B获取锁时,从主内存加载最新的flag(true)
System.out.println("感知到flag修改");
}
}
- 若不加
synchronized,线程 A 修改变量后可能未刷新到主内存,线程 B 一直读取工作内存中的旧值false,无法感知修改。 - 加
synchronized后,锁释放 / 获取的内存语义确保了flag的修改对其他线程可见。
3、保证有序性:基于互斥执行 + happens-before 规则
3.1. 有序性的定义
有序性指 程序执行的顺序与代码编写的顺序一致,避免因「指令重排序」导致的多线程执行混乱。JIT 编译器、CPU 为了优化性能,会在不影响单线程执行结果的前提下重排序指令,但多线程环境下可能导致错误。
3.2. synchronized 如何保证有序性?
核心逻辑:通过「互斥执行的串行化」和「JMM 的 happens-before 规则」,间接禁止指令重排序的可见性。
(1)互斥执行的串行化保障
由于同步块 / 方法同一时间只能被一个线程执行,相当于将多线程执行转化为「单线程串行执行」。而单线程环境下,指令重排序不会影响执行结果(as-if-serial 语义)------ 无论指令如何重排,单线程执行的最终结果与代码顺序一致,因此多线程通过 synchronized 执行时,不会出现因重排序导致的逻辑错误。
(2)happens-before 规则的强约束
JMM 定义了「监视器锁规则」:对同一个锁的解锁操作(unlock),happens-before 于后续对该锁的加锁操作(lock) 。
-
happens-before的含义:若操作 A happens-before 操作 B,则 A 的执行结果对 B 可见,且 A 的执行顺序在 B 之前(禁止 A 之后的指令重排序到 A 之前,也禁止 B 之前的指令重排序到 B 之后)。 -
具体效果:同步块内的所有操作(解锁前的操作),happens-before 于后续获取该锁的线程执行的操作(加锁后的操作)。这意味着:
- 同步块内的指令可以重排序,但不会重排序到同步块外部(解锁后)。
- 后续线程获取锁后,能看到前一个线程同步块内所有操作的结果(包括指令重排序后的正确结果)。
示例验证(避免重排序问题):
arduino
private int a = 0;
private boolean ready = false;
// 线程A执行
private synchronized void write() {
a = 1; // 操作1
ready = true;// 操作2
}
// 线程B执行
private synchronized void read() {
if (ready) { // 操作3
System.out.println(a); // 操作4,必然输出1
}
}
-
若不加
synchronized,CPU 可能将线程 A 的「操作 1 和操作 2」重排序(先执行ready=true,再执行a=1),导致线程 B 执行时ready=true但a=0,输出错误。 -
加
synchronized后:- 线程 A 的同步块内,操作 1 和操作 2 可重排序,但不会重排序到
write()方法外部(解锁后)。 - 「线程 A 解锁」happens-before「线程 B 加锁」,因此线程 B 执行时,必然能看到线程 A 操作 1 和操作 2 的最终结果(
a=1且ready=true),输出正确。
- 线程 A 的同步块内,操作 1 和操作 2 可重排序,但不会重排序到
4、总结:三个特性的核心保障逻辑
| 特性 | 核心保障机制 |
|---|---|
| 原子性 | Monitor 锁的互斥性:同步块 / 方法同一时间仅一个线程执行,操作不可分割。 |
| 可见性 | 锁释放 / 获取的内存语义:释放锁时刷新工作内存到主内存,获取锁时从主内存加载最新值。 |
| 有序性 | 1. 互斥执行转化为单线程串行(as-if-serial 语义);2. happens-before 规则约束指令重排序。 |
四、总结
1. 底层原理核心
synchronized 基于 Monitor 监视器锁 和 对象头 Mark Word 实现,通过竞争 Monitor 所有权保证线程互斥,锁状态存储在 Mark Word 中。
2. 优化机制核心
JDK 1.6+ 的优化核心是 "锁升级" :从偏向锁(无竞争)→ 轻量级锁(轻度竞争,自旋)→ 重量级锁(激烈竞争,阻塞),按需降低开销;配合自旋锁、锁消除、锁粗化等优化,让 synchronized 兼顾安全性和高性能。
3. 性能对比
- 无竞争场景:偏向锁 > 轻量级锁 > 重量级锁(偏向锁几乎无开销)。
- 轻度竞争场景:轻量级锁(自旋)> 重量级锁(避免内核态切换)。
- 激烈竞争场景:重量级锁(自旋无效,阻塞更高效)。