1. 引言:为什么 synchronized 仍然重要
在 Java 并发编程中,ReentrantLock、StampedLock、ConcurrentHashMap 等工具层出不穷,但 synchronized 从未退场。它是 JVM 内置监视器锁(Monitor) 的语法糖——每个 Java 对象都天然关联一把锁,无需额外分配锁对象,JVM 也对其做了大量针对性优化。
理解 synchronized,需要走过三层:
- 语法层:三种写法、字节码、
ACC_SYNCHRONIZED与monitorenter/monitorexit - 语义层:JMM 中的互斥、可见性、有序性,以及 happens-before
- 实现层:对象头 Mark Word、锁升级、ObjectMonitor,以及编译器的锁消除/粗化
2. 语言层:三种用法与字节码
2.1 三种 synchronized 写法
public class SyncDemo {
// 1. 同步实例方法 —— 锁对象是 this
public synchronized void instanceMethod() {
// ...
}
// 2. 同步静态方法 —— 锁对象是 Class 对象
public static synchronized void staticMethod() {
// ...
}
// 3. 同步代码块 —— 锁对象由括号内表达式决定
public void blockMethod() {
synchronized (this) {
// ...
}
}
}
等价关系:instanceMethod() 与 synchronized (this) { ... } 等价;staticMethod() 与 synchronized (SyncDemo.class) { ... } 等价。
锁对象的选择至关重要:只有多个线程竞争同一把锁,互斥才有意义。synchronized (new Object()) 每次创建新对象,等于没有同步。
2.2 字节码:两条不同的路径
synchronized 在字节码层的实现分为两条路径,不可混为一谈:
| 形式 | 字节码机制 |
|---|---|
synchronized 方法 | 方法 access_flags 置 ACC_SYNCHRONIZED(0x0020);JVM 在方法调用/返回时隐式加锁/解锁,不出现 monitorenter/monitorexit |
synchronized 代码块 | 编译器插入 monitorenter / monitorexit 指令对 |
ACC_SYNCHRONIZED 定义于 JVM 规范(HotSpot 中 JVM_ACC_SYNCHRONIZED = 0x0020),仅用于方法的 access flags。HotSpot 通过 AccessFlags::is_synchronized() 识别同步方法:
// accessFlags.hpp
bool is_synchronized() const { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }
2.3 javap 对比示例
public class BytecodeDemo {
public synchronized void syncMethod() { }
public void syncBlock() {
synchronized (this) { }
}
}
javap -v -c BytecodeDemo 输出(节选):
同步方法——flags 含 ACC_SYNCHRONIZED,Code 段无 monitor 指令:
public synchronized void syncMethod();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
同步代码块——显式 monitorenter/monitorexit,且编译器生成 try-finally 结构,保证异常路径也能释放锁:
public void syncBlock();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 加锁
4: aload_1
5: monitorexit // 正常路径释放
6: goto 14
9: astore_2
10: aload_1
11: monitorexit // 异常路径释放
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
两种路径在语义上等价:进入时加锁,退出时(含异常)释放锁。差异在于同步方法的加解锁由 JVM 在方法调用/返回处隐式完成,同步块则由 字节码指令 显式完成。
3. 监视器语义:可重入、wait/notify
3.1 可重入性
synchronized 是可重入锁:同一线程可以多次获取同一把锁,不会自死锁。
JVM 通过 递归计数 实现——重量级锁中 ObjectMonitor 的 _recursions 字段记录重入次数;轻量级锁场景下 displaced_header 置为 NULL 表示递归进入。
public class ReentrantDemo {
public synchronized void outer() {
inner(); // 同一线程再次获取 this 上的锁,合法
}
public synchronized void inner() {
// ...
}
}
3.2 wait / notify / notifyAll
Object.wait()、notify()、notifyAll() 必须在持有对应对象监视器锁的线程中调用,否则抛出 IllegalMonitorStateException。
标准用法(来自 java.lang.Object 文档):
synchronized (obj) {
while (!condition) {
obj.wait(); // 释放锁,进入等待;被唤醒后需重新竞争锁
}
// 条件满足,执行业务逻辑
}
为什么用 while 而不是 if?
- 虚假唤醒(spurious wakeup):线程可能在没有
notify的情况下被唤醒,OS 和 JVM 均不保证只唤醒一次。 - 多个等待者:
notify只唤醒一个线程,被唤醒时条件可能已被其他线程改变。 wait返回后需重新持锁:从wait返回只意味着重新获得了锁,不意味着条件成立。
wait() 释放锁并进入等待集;notify() 从 _WaitSet 移出一个等待者到入口队列;notifyAll() 移动全部等待者。这些在重量级锁的 ObjectMonitor 中实现(见 §7.5)。
4. JMM(Java 内存模型)与 synchronized
4.1 为什么需要 JMM
多线程环境下,编译器优化、CPU 乱序执行、各级缓存会导致:一个线程的写,另一个线程不一定立即可见。若缺乏形式化规则,并发程序的行为将无法推理。
JMM(JLS 第 17 章)定义了 happens-before(先行发生) 关系:若操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 在排序上先于 B。
4.2 synchronized 的三重保证
| 保证 | 含义 | 实现层面 |
|---|---|---|
| 互斥性 | 同一时刻至多一个线程持有同一把监视器锁 | Monitor enter/exit |
| 可见性 | 解锁前的写,对后续加锁线程可见 | 锁释放/获取时的内存同步 |
| 有序性 | 临界区内操作不会被重排到锁外 | 编译器/CPU 内存屏障约束 |
JMM 不保证所有线程的全局统一顺序,只保证 happens-before 链 可达的操作之间的可见性与有序性。
4.3 happens-before 规则
与 synchronized 直接相关的规则(摘自 JLS 与 java.util.concurrent 包文档):
- 程序次序规则:同一线程内,前面的操作 happens-before 后面的操作。
- 监视器锁规则:对监视器 M 的 unlock happens-before 后续对 M 的 lock。由于 hb 具有传递性,T1 在 unlock 前的所有操作 happens-before T2 在 lock 后的所有操作。
- volatile 规则(对比用):对 volatile 变量的写 happens-before 后续对该变量的读(无互斥)。
- 线程启动规则:
Thread.start()happens-before 被启动线程中的任意操作。 - 线程终止规则:线程中的所有操作 happens-before 其他线程在该线程上
join()的成功返回。
hb 传递链示意:
sequenceDiagram
participant T1 as Thread1
participant Lock as Monitor
participant T2 as Thread2
T1->>Lock: synchronized enter
T1->>Lock: write sharedVar
T1->>Lock: synchronized exit
Note over Lock: unlock hb lock
T2->>Lock: synchronized enter
T2->>Lock: read sharedVar传递链:T1 写 sharedVar →(程序次序)→ T1 unlock →(监视器锁规则)→ T2 lock →(程序次序)→ T2 读 sharedVar。因此 T2 一定能看到 T1 的写。
4.4 可见性问题:无锁 vs 有锁
反例——无 happens-before,可能永远看不到更新:
class Broken {
boolean flag = false;
int value = 0;
void writer() {
value = 42;
flag = true; // 与 reader 无 hb 关系
}
void reader() {
while (!flag) { /* spin */ }
System.out.println(value); // 可能输出 0
}
}
修复——同一把锁建立 hb:
class Fixed {
private final Object lock = new Object();
boolean flag = false;
int value = 0;
void writer() {
synchronized (lock) {
value = 42;
flag = true;
}
}
void reader() {
synchronized (lock) {
if (flag) {
System.out.println(value); // 保证看到 42
}
}
}
}
4.5 数据竞争
JLS 定义:两个线程访问同一变量,至少一个是写,且没有 happens-before 关系,即构成数据竞争,属于未定义行为。
synchronized 通过互斥 + hb 消除受保护变量的数据竞争。但需注意边界——不同锁之间没有 hb:
synchronized (lockA) { x = 1; }
synchronized (lockB) { int y = x; } // 不保证看到 1
4.6 synchronized 与 volatile 的 JMM 对比
| 维度 | synchronized | volatile |
|---|---|---|
| 互斥 | 是 | 否 |
| 可见性 | 是(unlock→lock hb) | 是(写→读 hb) |
| 适用场景 | 复合操作(check-then-act) | 单一变量的状态标志 |
| 阻塞 | 可能阻塞 | 不阻塞 |
JLS 指出:volatile 的读写「具有与进入/退出监视器类似的内存一致性效果,但不涉及互斥锁定」。
4.8 有序性
单线程内,JMM 保证 as-if-serial 语义。synchronized 块内,编译器和 CPU 不能将块内操作重排到块外。但两个相邻 synchronized 块之间,块外操作仍可能在各自块边界处被重排。
4.9 小结
synchronized 的本质是通过监视器锁规则在 unlock 与后续 lock 之间建立 happens-before。使用时须明确:保护哪些共享变量、是否使用同一把锁。需要可中断、超时、多条件队列时,应考虑 Lock(见 §6)。
5. 常见误区与最佳实践
| 误区 | 正确做法 |
|---|---|
锁错对象(synchronized(new Object()) 每次新建) | 使用共享的 final 锁对象 |
| 在 synchronized 内做耗时 IO | 缩小临界区,只保护共享状态 |
| 混用 wait 与 sleep | wait 释放锁,sleep 不释放 |
| 认为 synchronized 一定慢 | 无竞争时 JVM 有锁消除/粗化等优化(见 §7.7) |
java.util.concurrent 文档指出:高并发场景下,ConcurrentHashMap 通常优于 Collections.synchronizedMap;但若并发度不高,后者也足够简单可用。选型取决于竞争程度与 API 需求,而非「synchronized 已过时」。
6. 与 Lock / j.u.c 的对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁释放 | 自动(块结束/方法返回) | 手动 unlock(),需 finally |
| 公平性 | 非公平 | 可选公平/非公平 |
| 可中断 | 否 | lockInterruptibly() |
| 超时 | 否 | tryLock(timeout) |
| 条件队列 | 单一(wait/notify) | 多个 Condition |
选型建议:
- 简单互斥、临界区短 →
synchronized(代码简洁,JVM 优化充分) - 需要可中断、超时、公平锁、多条件等待 →
ReentrantLock - Virtual Thread 场景下长时间阻塞 → 优先
ReentrantLock或缩小 synchronized 范围(见 §8)
7. HotSpot 实现(JDK 8 源码)
7.1 对象头结构
HotSpot 中普通 Java 对象(oopDesc)头部至少包含两部分:
┌─────────────────┬─────────────────┐
│ Mark Word │ Klass Pointer │ ← 普通对象(64 位)
│ (8 bytes) │ (8 bytes) │
└─────────────────┴─────────────────┘
- Mark Word:哈希码、GC 年龄、锁状态等;synchronized 的锁信息主要存储于此
- Klass Pointer:指向类元数据(
-XX:+UseCompressedOops时为 4 字节 narrowKlass) - 数组对象在 Klass Pointer 之后还有 array length 字段
oopDesc 结构:
// oop.hpp
class oopDesc {
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// ...
};
7.2 Mark Word 位布局与锁状态
markOop.hpp 注释描述了 64 位 JVM 下的布局:
正常对象(无锁):
unused:25 | hash:31 | unused:1 | age:4 | biased_lock:1 | lock:2
偏向锁:
JavaThread*:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
lock:2 末两位 — 四种状态:
| lock:2 | 状态 | Mark Word 含义 |
|---|---|---|
| 01 | 无锁 / 偏向锁 | 常规 header 或偏向线程指针 |
| 00 | 轻量级锁 | ptr 指向线程栈上 BasicLock |
| 10 | 重量级锁 | ptr 指向 ObjectMonitor |
| 11 | GC 标记 | 用于 markSweep,非常态 |
源码注释原文:
// markOop.hpp
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
同一 Mark Word 在不同状态下复用各 bit 字段:hash(identity hashCode)、age(GC 年龄,最大 15)、biased_lock + epoch(偏向锁代际)。
7.3 锁升级路径(JDK 8)
flowchart LR
noLock[无锁] --> biased[偏向锁]
biased --> light[轻量级锁]
light --> heavy[重量级锁]
biased -->|"撤销 bias"| light- 偏向锁(
biasedLocking.hpp):同一线程反复加锁,Mark Word 记录线程 ID,无需 CAS - 轻量级锁:CAS 将 Mark Word 复制到线程栈
BasicLock,成功则栈上锁定 - 重量级锁:竞争失败时 inflate 为
ObjectMonitor
锁只能升级,不能降级(偏向锁撤销除外)。JDK 15 起偏向锁逐步废弃(见 §8)。
7.4 加锁/解锁核心流程
synchronizer.cpp 是监视器 enter/exit 的核心实现:
fast_enter — 先处理偏向锁,再进入 slow_enter:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
BiasedLocking::revoke_at_safepoint(obj);
}
}
slow_enter (obj, lock, THREAD) ;
}
slow_enter — CAS 抢轻量级锁,失败则 inflate:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
if (mark->is_neutral()) {
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
return; // CAS 成功,轻量级锁
}
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
lock->set_displaced_header(NULL); // 可重入
return;
}
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
fast_exit — CAS 恢复 Mark Word;失败走重量级 exit:
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
markOop dhw = lock->displaced_header();
if (dhw == NULL) {
return; // 递归退出,无需操作
}
markOop mark = object->mark();
if (mark == (markOop) lock) {
if ((markOop) Atomic::cmpxchg_ptr(dhw, object->mark_addr(), mark) == mark) {
return; // 轻量级解锁成功
}
}
ObjectSynchronizer::inflate(THREAD, object, inflate_cause_vm_internal)->exit(true, THREAD);
}
7.5 ObjectMonitor 与 wait/notify
轻量级锁竞争失败时,对象头膨胀为指向 ObjectMonitor 的指针。关键字段:
| 字段 | 含义 |
|---|---|
_owner | 当前持有锁的线程 |
_recursions | 重入次数 |
_WaitSet | 调用 wait() 的线程队列 |
_EntryList | 阻塞等待锁的线程队列 |
_cxq | 竞争队列(Contention Queue) |
wait 流程:释放锁 → 线程加入 _WaitSet → park 阻塞 → 被 notify 后移入 _EntryList → 重新竞争锁 → 从 wait() 返回。
notify 流程:从 _WaitSet 移出一个(或全部)线程到 _EntryList,unpark 唤醒。
7.6 膨胀(Inflate)触发场景
synchronizer.hpp 定义了膨胀原因:
typedef enum {
inflate_cause_vm_internal = 0,
inflate_cause_monitor_enter = 1,
inflate_cause_wait = 2,
inflate_cause_notify = 3,
inflate_cause_hash_code = 4,
inflate_cause_jni_enter = 5,
inflate_cause_jni_exit = 6,
} InflateCause;
典型触发:多线程竞争同一把轻量级锁、wait/notify、计算 identity hashCode(可能与锁 bits 冲突)、JNI 监视器操作等。
7.7 JVM 锁优化策略
HotSpot 对 synchronized 的优化分两层:运行时锁升级(§7.3)与 编译器锁优化(C2 为主)。
flowchart TB
subgraph compileTime [编译期 C2 优化]
LE[锁消除]
LC[锁粗化]
NL[嵌套锁消除]
end
subgraph runtime [运行时优化]
BL[偏向锁]
LL[轻量级锁 + CAS]
SP[自适应自旋]
DF[Monitor deflation]
end
LE -->|"对象未逃逸"| noLockPath[无锁执行]
LC -->|"合并相邻 lock/unlock"| fewerOps[减少加锁次数]
NL -->|"同对象重复加锁"| outerOnly[只保留最外层]
BL --> LL --> SP --> heavy[重量级锁 ObjectMonitor]
heavy --> DF(1)锁消除(Lock Elimination)
条件:C2 逃逸分析(-XX:+DoEscapeAnalysis)判定锁对象未逃逸出当前线程,则 lock/unlock 对其他线程不可见,同步无必要。
实现:ConnectionGraph::optimize_ideal_graph() 将锁标记为 NonEscObj,阻止 macro 展开,编译后去掉加解锁。
public String concat(String a, String b) {
StringBuffer sb = new StringBuffer(); // sb 未逃逸
sb.append(a); // StringBuffer 内部 synchronized 可能被消除
sb.append(b);
return sb.toString();
}
callnode.cpp 中的判断逻辑:
if (can_reshape && EliminateLocks && !is_non_esc_obj()) {
ConnectionGraph *cgr = phase->C->congraph();
if (cgr != NULL && cgr->not_global_escape(obj_node())) {
this->set_non_esc_obj(); // 标记消除
return result;
}
}
对象若逃逸(作为返回值、写入静态字段、传入未知方法),则不能消除。反优化(deoptimization)时可能需要 relock 恢复监视器状态。
(2)锁粗化(Lock Coarsening)
思路:相邻多次对同一对象加锁/解锁,合并为一次更大范围的锁。
// JIT 可能将下面两段粗化为一个 synchronized 块
synchronized (obj) { doA(); }
synchronized (obj) { doB(); }
实现:C2 AbstractLockNode::Ideal() 中 find_matching_unlock() 将 lock-unlock-lock 序列标记为 Coarsened。
HotSpot 参数 -XX:+EliminateLocks(默认开启)的描述正是 “Coarsen locks when possible”。粗化会扩大锁持有范围,JVM 仅在判定合并更划算时应用。
(3)嵌套锁消除(Nested Lock Elimination)
同一线程对同一对象的嵌套 synchronized,内层 lock/unlock 可被消除:
synchronized (obj) {
synchronized (obj) { // 内层可能被消除
// ...
}
}
由 -XX:+EliminateNestedLocks(默认开启)控制,LockNode::is_nested_lock_region() 检测嵌套区域。
(4)运行时:偏向锁 / 轻量级锁 / 自旋
| 策略 | 作用 | 关键参数 |
|---|---|---|
| 偏向锁 | 单线程反复进入,Mark Word 记录线程 ID | -XX:+UseBiasedLocking(JDK 8 默认) |
| 轻量级锁 | 无竞争时 CAS 抢栈锁 | 自动 |
| 自适应自旋 | 重量级锁竞争时 spin 再 park | ObjectMonitor 中 Knob_PreSpin |
自旋发生在 inflate 为 ObjectMonitor 之后、线程 park 之前。
(5)Monitor 收缩(Deflation)
重量级锁膨胀后,若无竞争,在 Safepoint 时 ObjectSynchronizer::deflate_idle_monitors() 回收 ObjectMonitor,Mark Word 恢复为无锁/轻量级状态,与 inflate 对称。
(6)优化策略对比
| 策略 | 阶段 | 默认 | 主要 JVM 参数 | 效果 |
|---|---|---|---|---|
| 锁消除 | C2 编译 | 开 | DoEscapeAnalysis + EliminateLocks | 去掉整个 lock/unlock |
| 锁粗化 | C2 编译 | 开 | EliminateLocks | 减少中间 unlock/lock |
| 嵌套锁消除 | C2 编译 | 开 | EliminateNestedLocks | 去掉内层 lock/unlock |
| 偏向锁 | 运行时 | 开(JDK8) | UseBiasedLocking | 减少 CAS |
| 轻量级锁 | 运行时 | 开 | — | 避免 inflate |
| 自适应自旋 | 运行时 | 开 | — | 减少 park/唤醒 |
| Monitor 收缩 | Safepoint | 开 | — | 回收空闲 Monitor |
这些优化对开发者透明。理解它们有助于解释「无竞争 synchronized 为何很快」,但不能依赖锁消除保证正确性——并发正确性仍须按 JMM 规则编写代码。
8. 现代 JDK 演进(与 JDK 8 对比)
阅读 JDK 8 源码时,需注意以下版本差异:
| 变化 | 说明 |
|---|---|
| 偏向锁废弃 | JDK 15 默认关闭(-XX:-UseBiasedLocking),JDK 18 移除;现代路径为无锁 → 轻量级 → 重量级 |
| 锁优化持续 | C2 的 fast_lock 内联路径持续演进,但 Mark Word + ObjectMonitor 核心模型不变;锁消除/粗化/嵌套锁消除仍有效 |
| Virtual Threads(JEP 444) | synchronized 块内阻塞会导致 pinning——载体平台线程无法释放去执行其他虚拟线程。长时间阻塞建议改用 ReentrantLock,或尽量缩小 synchronized 范围 |
| 诊断工具 | JFR 的 JavaMonitorEnter 事件、async-profiler 的 lock profiling 等,可用于观察锁竞争与持有时间 |
Virtual Thread pinning 示例:
// 虚拟线程在 synchronized 内阻塞时,底层平台线程被"钉住"
Thread.startVirtualThread(() -> {
synchronized (lock) {
Thread.sleep(10_000); // pinning:平台线程无法服务其他虚拟线程
}
});
注意:该问题在 JDK 24 (JEP 491)中已经被正式修复,通过改变 HotSpot VM 对对象监视器(synchronized 锁)的实现,使得虚拟线程在 synchronized 块内阻塞时,也能够释放其载体线程(平台线程)。
对于 I/O 密集型、大量虚拟线程的场景,优先使用 ReentrantLock + lockInterruptibly,或基于 java.util.concurrent 的非阻塞结构。
9. 总结
理解 synchronized 的三层模型:
- 语法层:
ACC_SYNCHRONIZED(方法)与monitorenter/monitorexit(代码块) - 语义层:JMM 中通过监视器锁规则建立 happens-before,提供互斥、可见性、有序性
- 实现层:对象头 Mark Word 存储锁状态,无竞争时偏向/轻量级锁,竞争时膨胀为 ObjectMonitor;C2 编译器还可做锁消除、粗化、嵌套锁消除
实践建议:
- 默认场景用
synchronized足够简洁且性能良好 - 高竞争场景考虑
j.u.c细粒度工具或减小锁粒度 - Virtual Thread 场景避免在 synchronized 内长时间阻塞
- 不要为触发锁消除而刻意写代码——优化是 JIT 的事,正确性靠 JMM