1 synchronized 的三种使用方式
synchronized 是 Java 最基础、最安全的同步方案,它使用 对象内置锁(Monitor Lock) 来保证多个线程访问临界区时的互斥性。
1.1 修饰实例方法
写法:
public synchronized void m() {
count++;
}
锁对象是 this(当前实例),对应对象头的 Monitor。
特性如下:
多线程访问同一个对象实例时会阻塞。
如果是不同对象实例,则不互斥,因为每个对象头对应不同的 Monitor。
锁粒度较大,容易导致整个对象方法被锁住而降低并发性。
适合对象本身内部状态一致性很重要的场景(如银行账户类)。
示例场景:
两个 ATM 操作同一个账户对象时需要锁,但操作不同账户对象不需要锁。
1.2 修饰静态方法
public static synchronized void log() {
...
}
锁对象是类对象,即 XXX.class,它在 JVM 中是单例的。
因此:
所有实例调用该静态方法时都要争同一把锁。
适用于控制全局资源(文件日志、单例变量等)。
实际上:
synchronized 修饰实例方法 → 锁的是 对象监视器锁
synchronized 修饰静态方法 → 锁的是 Class 监视器锁
它们之间互不干扰。
1.3 修饰代码块(最灵活)
synchronized (lockObj) {
// 临界区
}
锁对象是 lockObj 引用指向的对象。
为什么推荐使用代码块?
锁粒度可控,只锁住关键代码,不锁整个方法。
可以独立构造专门的锁对象,避免与其他 synchronized 冲突。
避免锁 this(因为某些外部代码可能也锁 this,导致意外形成死锁)。
示例推荐写法:
private final Object lock = new Object(); // 永远不要用 new Object() 每次创建
public void update() {
synchronized (lock) {
...
}
}
锁对象一定要保证:
是 final 的
不暴露在外部(否则外部也能锁这个对象)
2 synchronized 的底层原理(字节码与 Monitor)
理解 synchronized 的执行过程,需要从字节码和 JVM 内部结构来看。
2.1 monitorenter 与 monitorexit 字节码
示例:
synchronized (obj) {
x++;
}
编译后:
monitorenter
... // 临界区代码
monitorexit
特点:
每个 synchronized 代码块有且必须有一条 monitorenter,与至少一条 monitorexit。
编译器会自动生成 try-finally,保证 monitorexit 总会执行。
如果遗漏 monitorexit,会导致锁永远不释放(因此 Java 语法禁止开发者绕过)。
为什么两条 monitorexit?
因为 finally 中也会插入一条,以保证异常时一样释放。
2.2 Monitor 的内部机制(HotSpot 实现)
Monitor 是 JVM 层面的锁,与 synchronized 直接关联。
Monitor 内部包含:
owner:当前占有锁的线程(为空表示锁空闲)
entryList:尝试竞争锁但竞争失败的线程队列
cxq:新来的竞争线程队列,后来加入时通常先入 cxq
waitSet:执行了 wait() 的线程队列(放弃锁)
递归计数器:用来支持同一线程的可重入
当 monitorenter 发生时:
若 owner == null:获得锁,owner = 当前线程
若 owner == 当前线程:递归获取(计数 +1)
若 owner == 其他线程:进入 EntryList 或 cxq 阻塞
这个机制也能解释:
为什么 synchronized 是可重入的
为什么 wait() 必须在 synchronized 内部调用(要进入 WaitSet 前必须先获得锁)
3 JVM 对象头与 Lock Record、Monitor 的交互
synchronized 的锁状态完全记录在对象头(Object Header)与线程栈帧的 Lock Record 中。
3.1 对象头结构(HotSpot MarkWord)
Mark Word 用来记录锁状态、hashCode、GC Age、偏向线程 ID 等信息。
不同锁状态下,Mark Word 内容不同。
例如 64 位 JVM 中典型结构如下(示意):
无锁:
hashcode(31bit) | age(4bit) | biased_flag(1) | lock_state(2bits=01)
偏向锁:
threadId(54bits) | epoch(2) | age(4) | biased_flag | lock_state(01)
轻量级锁:
ptr to LockRecord | lock_state(00)
重量级锁:
ptr to Monitor | lock_state(10)
Mark Word 的变化就是锁升级的真实来源。
3.2 Lock Record(轻量级锁用)
轻量级锁依赖线程栈帧中专门的 Lock Record 每个 synchronized 块进入时都会在当前线程栈中创建。
Lock Record 内保存:
指向 Mark Word 的原始值
指向锁对象
锁状态记录
轻量级锁过程:
线程进入同步区 → 在当前线程栈中创建 Lock Record
CAS 尝试把对象头 Mark Word 指向 Lock Record
如果成功 → 获得轻量级锁
如果失败 → 说明竞争 → 进入自旋或升级为重量级锁
4 JVM 锁优化与升级流程(核心)
HotSpot 对 synchronized 做了大量优化,使其性能远比 JDK 1.4 时代好。
锁的升级流程如下:
无锁
↓
偏向锁
↓
轻量级锁(自旋锁)
↓
重量级锁(OS Mutex)注意:不会降级,只会升级。
以下详细解释每一种状态。
4.1 偏向锁(Biased Lock)
偏向锁的目的:尽可能让无竞争的 synchronized 更快。
适用于某个对象总是由同一个线程访问的情况。
偏向锁流程:
对象第一次被线程 T1 进入 synchronized
JVM 会直接把线程 ID 写入 Mark WordT1 再次进入 synchronized
JVM 发现线程 ID 相同 → 不做任何锁操作
连 CAS 都不执行,几乎零成本当 T2 第一次竞争这个锁 → 撤销偏向锁
偏向锁撤销过程比较复杂:
JVM 到达 safepoint(暂停所有线程)
检查所有线程的栈帧,看锁对象是否被其他线程持有
恢复 Mark Word(取消偏向信息)
升级为轻量级锁
偏向锁适合:
同步块短
几乎无竞争
单线程频繁进入
这是目前 JVM 加速 synchronized 最重要的一环。
4.2 轻量级锁(自旋锁)
轻量级锁通过 CAS 与自旋来避免线程阻塞。
为什么自旋而不是直接阻塞?
阻塞涉及 OS 线程调度,开销非常大(几十倍于 CAS)。
轻量级锁流程如下:
线程 T1 在栈中创建 Lock Record
使用 CAS 将对象头指向 Lock Record 的地址
CAS 成功 → 获得轻量级锁
T2 也来获取锁 → CAS 失败 → T2 进行自旋
自旋一定次数(一般几十次,依 JVM 配置)
若 T1 释放锁前 T2 自旋成功 → 获取轻量级锁
否则 → 锁膨胀为重量级锁
轻量级锁的核心:
尽可能不用 OS 的阻塞机制,因此在短时间的锁竞争下效率高。
4.3 重量级锁(OS Mutex)
轻量级锁无法解决长时间竞争,因此会升级为重量级锁。
重量级锁特点:
依赖 OS 层面的 mutex 或 futex
获取不到锁的线程会真正阻塞(挂起)
唤醒线程需要 OS 调度(开销非常大)
稳定但速度最慢
Monitor 的 EntryList 会阻塞线程,而不是让它们忙等待。
重量级锁适合:
高并发
临界区长
多线程激烈竞争
5 synchronized 的关键特性
5.1 可重入(Reentrant)
可重入意味着同一线程可以重复获得同一把锁而不会死锁。
如:
public synchronized void a() {
b();
}
public synchronized void b() {}
Monitor 内部记录递归次数:
第一次获取锁 → count = 1
调用 b() → count = 2
方法退出 → count = 1
再退出 → count = 0 → 释放锁
5.2 内存语义(可见性 + 有序性 + 原子性)
进入 synchronized:
工作内存失效 → 重新从主内存读取共享变量
退出 synchronized:
将修改后的变量刷新到主内存
因此:
提供可见性(类似 volatile)
保证原子性(临界区内不会被打断)
保证有序性(同步块内指令不被重排)
5.3 锁自动释放
当同步块退出(正常或异常)时,JVM 自动执行 monitorexit。
不会出现 ReentrantLock 忘记 unlock 的问题。
6 wait/notify 与 Monitor 的结合(更细节)
Monitor 有两个队列:
EntryList:等待获取锁的线程(阻塞)
WaitSet:调用了 wait() 的线程(放弃锁)
wait/notify 规则:
wait() 必须在 synchronized 内调用
wait() 会释放锁,并进入 WaitSet
notify() 将 WaitSet 的某个线程移动到 EntryList
只有当调用 notify 的线程退出 synchronized 后,唤醒的线程才可能获得锁
注意:notify 不一定唤醒最早的线程,其选择通常是不确定的。
如果你需要有序唤醒,可以使用 Condition(ReentrantLock)替代。
7 synchronized 与 ReentrantLock 对比(更全面)
总结使用建议:
如果只是简单同步:
推荐 synchronized(JVM 针对它做了大量优化)。
如果需要复杂锁机制:
使用 ReentrantLock。