吃透synchronized锁机制:从使用到底层,一文破解Java线程安全难题
一、synchronized的三大特性深度解析
原文提到了原子性、可见性、有序性,这里补充JMM层面的实现机制,这是面试官最想听到的深度。
1.1 原子性的底层保障:内存屏障
synchronized的原子性并非简单“互斥”,而是通过JVM插入的内存屏障实现的。在synchronized代码块前后,JVM会插入以下屏障:
| 屏障类型 | 插入位置 | 作用 |
|---|---|---|
| LoadLoad屏障 | 获取锁后 | 禁止后续读操作重排序到锁之前 |
| StoreStore屏障 | 释放锁前 | 确保写操作在释放锁前完成 |
| LoadStore屏障 | 获取锁后 | 禁止读操作重排序到写操作之前 |
| StoreLoad屏障 | 释放锁后 | 确保所有写操作对其他线程可见 |
关键点:这些屏障确保了锁内的代码像一个不可分割的“事务”被执行。
1.2 可见性的实现机制:Happens-Before原则
synchronized满足JMM的Happens-Before规则中的管程锁定规则:
对一个锁的解锁操作,happens-before于随后对同一个锁的加锁操作。
这意味着:
线程A释放锁时,会将工作内存中的共享变量刷新到主内存
线程B获取同一把锁时,会从主内存重新加载共享变量
因此线程B一定能看到线程A修改的结果
底层实现:在x86架构上,monitorexit指令会触发LOCK前缀指令,该指令会强制将CPU缓存写入主内存,并让其他CPU的缓存行失效。
1.3 有序性的保障:禁止指令重排序
synchronized禁止重排序的范围仅限于被锁定的代码块内部,而非整个程序。JVM通过以下机制保证:
锁获取屏障:确保获取锁前的操作不会重排序到锁内
锁释放屏障:确保锁内的操作不会重排序到锁外
示例:
java
int a = 0; synchronized (lock) { a = 1; // 不会与锁外的操作重排序 } int b = a; // 能保证读到a=1二、对象头与Monitor的深度拆解
原文提到了对象头和Monitor,这里补充64位JVM的对象头结构和Monitor的内部实现。
2.1 64位JVM的Mark Word结构
64位JVM的Mark Word长度为8字节(64位),不同锁状态下结构不同:
| 锁状态 | 锁标志位 | 64位Mark Word结构 |
|---|---|---|
| 无锁 | 01 | unused:25 | hashCode:31 | cms_free:1 | age:4 | biased_lock:0 | lock:01 |
| 偏向锁 | 01 | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:01 |
| 轻量级锁 | 00 | ptr_to_lock_record:62 | lock:00 |
| 重量级锁 | 10 | ptr_to_monitor:62 | lock:10 |
| GC标记 | 11 | 用于垃圾回收标记 |
关键字段说明:
hashCode:对象的哈希码(未重写时)age:GC分代年龄(4位,最大15)biased_lock:是否偏向锁标志thread:持有偏向锁的线程IDptr_to_lock_record:指向栈中锁记录的指针ptr_to_monitor:指向Monitor对象的指针
2.2 Monitor的内部实现
Monitor是JVM内部的一个C++对象(ObjectMonitor),其核心数据结构:
cpp
class ObjectMonitor { volatile markOop _header; // 对象头 void* _owner; // 当前持有锁的线程 ObjectWaiter* _WaitSet; // 等待队列(wait()方法) ObjectWaiter* _EntryList; // 阻塞队列(等待获取锁) volatile intptr_t _count; // 锁计数器(重入次数) // ... };执行流程:
线程尝试获取锁时,CAS将
_owner设置为当前线程如果
_owner已被占用,线程进入_EntryList阻塞持有锁的线程调用
wait(),进入_WaitSet,释放锁其他线程调用
notify(),将_WaitSet中的线程移动到_EntryList
三、锁升级机制的深入细节
原文介绍了锁升级的四个阶段,这里补充每个阶段的触发条件和底层实现细节。
3.1 偏向锁的撤销与批量重偏向
偏向锁撤销不是简单的“升级”,而是有复杂的批量机制:
| 场景 | 行为 | 原因 |
|---|---|---|
| 第1-19次撤销 | 偏向锁撤销,升级为轻量级锁 | 轻度竞争,避免重偏向开销 |
| 第20次撤销 | 触发批量重偏向 | JVM认为该类可能有偏向价值 |
| 第40次撤销 | 触发批量撤销,该类禁用偏向锁 | JVM认为该类不适合偏向锁 |
配置参数:
-XX:BiasedLockingStartupDelay=0:JVM启动后立即启用偏向锁(默认延迟4秒)-XX:-UseBiasedLocking:禁用偏向锁(高并发场景可禁用,减少撤销开销)
3.2 轻量级锁的自旋优化
轻量级锁使用CAS自旋等待,自旋次数不是固定的:
| 自旋策略 | 说明 |
|---|---|
| 固定自旋 | JDK 1.6之前,默认自旋10次 |
| 自适应自旋 | JDK 1.6+,JVM根据历史自旋成功率动态调整 |
| 自旋锁膨胀 | 自旋失败一定次数后,升级为重量级锁 |
自适应自旋逻辑:
如果上次自旋成功过,本次自旋次数可以更多
如果上次自旋失败,本次自旋次数减少,快速升级
3.3 锁升级的完整流程图
text
对象创建 ↓ 无锁状态 ↓ 线程首次获取锁 ↓ ┌──────────────┴──────────────┐ ↓ ↓ 偏向锁获取成功 偏向锁撤销(有竞争) (Mark Word记录线程ID) ↓ ↓ 轻量级锁 同一线程重入 ↓ (无需CAS) CAS自旋获取锁 ↓ ↓ 无锁竞争 自旋失败多次 ↓ ↓ └──────────────┬──────────────┘ ↓ 重量级锁 (Monitor,内核态阻塞)
四、synchronized的性能优化:锁消除与锁粗化
除了锁升级,JVM还有两个重要的synchronized优化机制,很多开发者不知道。
4.1 锁消除(Lock Elimination)
JVM的逃逸分析发现某个锁对象永远不会被其他线程访问时,会消除该锁。
示例:
java
public void test() { Object lock = new Object(); // 局部对象,线程私有 synchronized (lock) { System.out.println("这个锁会被消除"); } }JVM参数:-XX:+EliminateLocks(JDK 1.8默认开启)
4.2 锁粗化(Lock Coarsening)
JVM会将连续的同步代码块合并为一个大的同步块,减少锁获取/释放次数。
示例(优化前):
java
public void test() { for (int i = 0; i < 100; i++) { synchronized (lock) { count++; } } }优化后(锁粗化):
java
public void test() { synchronized (lock) { for (int i = 0; i < 100; i++) { count++; } } }五、synchronized vs ReentrantLock 深度对比
原文给出了对比表格,这里补充底层实现和适用场景的深度分析。
5.1 底层实现对比
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM层(C++实现) | JDK层(Java实现) |
| 锁获取 | monitorenter指令 | Unsafe类的CAS |
| 线程阻塞 | 进入_EntryList阻塞 | 调用LockSupport.park() |
| 公平性 | 默认非公平,不可配置 | 可配置公平/非公平 |
| 条件变量 | 只有一个等待队列(wait/notify) | 可有多个Condition |
5.2 功能对比
| 功能 | synchronized | ReentrantLock |
|---|---|---|
| 可重入 | ✅ | ✅ |
| 中断响应 | ❌(无法中断等待锁的线程) | ✅(lockInterruptibly()) |
| 超时获取 | ❌ | ✅(tryLock(timeout)) |
| 公平锁 | ❌ | ✅(构造参数) |
| 多条件等待 | ❌(只有wait/notify) | ✅(多个Condition) |
| 锁状态查询 | ❌ | ✅(isHeldByCurrentThread()) |
5.3 性能对比与选择建议
性能对比(JDK 1.8+,JVM经过大量优化):
低竞争场景:synchronized ≈ ReentrantLock(甚至略优)
高竞争场景:两者接近,但ReentrantLock的
tryLock可以避免阻塞
选择建议:
优先使用synchronized:代码简洁,JVM持续优化,满足90%场景
使用ReentrantLock的场景:
需要可中断获取锁(
lockInterruptibly)需要超时获取锁(
tryLock)需要公平锁
需要多个条件变量(
Condition)
六、wait/notify机制与synchronized的配合
synchronized与wait()/notify()是Java经典的生产者-消费者模式基础,很多开发者用不好。
6.1 核心规则
必须先获取锁:
wait()/notify()必须在synchronized代码块中调用调用wait()会释放锁:线程进入
_WaitSet,释放Monitor调用notify()不释放锁:只将等待线程移动到
_EntryList,当前线程继续执行直到释放锁
6.2 正确使用示例
java
public class WaitNotifyDemo { private final Object lock = new Object(); private boolean condition = false; // 等待线程 public void waitForCondition() throws InterruptedException { synchronized (lock) { while (!condition) { // 必须用while,不能用if lock.wait(); // 释放锁,进入等待 } // 条件满足,继续执行 } } // 唤醒线程 public void signalCondition() { synchronized (lock) { condition = true; lock.notifyAll(); // 唤醒所有等待线程(notify只能唤醒一个) } } }为什么必须用while而不是if:
被唤醒后,条件可能再次被其他线程改变
while可以重新检查条件,避免“虚假唤醒”
6.3 常见错误:notify丢失
java
// 错误示例 synchronized (lock) { if (condition) { lock.wait(); // 条件不满足才等待 } // 但这里可能condition被其他线程改变 } // 正确做法:while检查 synchronized (lock) { while (condition) { // 条件满足时等待 lock.wait(); } }七、面试高频问题与深度解析
7.1 被synchronized修饰的方法,抛出异常后锁会释放吗?
答案:会释放。
原因:JVM编译时,会在异常表中插入monitorexit指令,确保异常路径也能释放锁。这就是字节码中有两个monitorexit的原因。
7.2 synchronized锁的是对象还是代码?
答案:锁的是对象(确切地说是对象的Monitor),不是代码。
证明:
java
public class Test { public synchronized void method1() { } public synchronized void method2() { } }同一个实例调用method1和method2,两个方法会互斥——因为锁的是同一个this对象。
7.3 String作为锁对象有什么问题?
问题:
字符串常量池:
"lock"字面量在常量池中,可能被其他代码意外共享不可变性:虽然不可变,但不同包名类可能使用相同字符串
正确做法:
java
// 不推荐 private final String lock = "lock"; // 推荐 private final Object lock = new Object();
7.4 synchronized与volatile的区别?
| 维度 | synchronized | volatile |
|---|---|---|
| 原子性 | ✅ 保证 | ❌ 不保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证(禁止重排序) |
| 锁机制 | 互斥锁 | 无锁 |
| 性能 | 开销大 | 开销小 |
| 适用场景 | 复合操作(如count++) | 单一读/写操作 |
八、总结:synchronized的核心思维框架
text
synchronized = 互斥 + 可见性 + 有序性 互斥的实现 ├── 字节码:monitorenter/monitorexit ├── 对象头:Mark Word锁状态位 └── Monitor:_owner、_EntryList、_WaitSet 锁升级机制 ├── 无锁 → 偏向锁:单线程重复获取 ├── 偏向锁 → 轻量级锁:少量线程竞争 └── 轻量级锁 → 重量级锁:激烈竞争、自旋失败 性能优化 ├── 锁消除:逃逸分析消除无用锁 ├── 锁粗化:合并连续同步块 └── 自适应自旋:动态调整自旋次数 使用原则 ├── 锁对象不可变(final修饰) ├── 锁粒度最小化(优先用代码块) ├── 保护静态资源用类锁 └── 避免死锁(统一锁顺序)
核心结论:
synchronized是JVM内置锁,自动获取释放,使用简单
锁升级机制是JDK 1.6的核心优化,按需升级减少开销
锁对象的选择决定锁的作用范围(实例锁 vs 类锁)
锁粒度控制直接影响并发效率,优先用代码块
JDK 1.8之后,synchronized性能已接近ReentrantLock,简单场景优先使用
synchronized虽然基础,但深入理解其底层实现、锁升级机制、JVM优化,是成为Java并发编程高手的必经之路。希望这份深度整理能帮你建立起完整的synchronized知识体系,写出更安全、更高效的并发代码。
