外观
synchronized加锁的原理
⭐ 题目日期:
美团 - 2025/4/4
📝 题解:
synchronized
是 Java 中实现线程同步的核心机制,其底层原理涉及 对象锁、JVM 字节码指令 和 操作系统级互斥。以下是其工作原理的详细分析:
1. 基本概念:锁与监视器(Monitor)
- 锁的本质:每个 Java 对象都与一个 监视器锁(Monitor) 关联,
synchronized
通过竞争对象的 Monitor 实现同步。 - Monitor 结构:
- Entry Set:存放等待锁的线程。
- Owner:持有锁的线程。
- Wait Set:存放调用
wait()
后释放锁的线程。
2. 底层实现:JVM 字节码
修饰代码块:
- 编译后会在代码块前后插入
monitorenter
和monitorexit
指令。 - 示例:
public void method() { synchronized(obj) { // 代码块 } }
// 字节码反编译结果: monitorenter // 尝试获取锁 ... // 同步代码 monitorexit // 释放锁
- 编译后会在代码块前后插入
修饰方法:
- 方法会添加
ACC_SYNCHRONIZED
标志,JVM 在方法调用时自动加锁。
- 方法会添加
3. 锁的存储位置:对象头(Mark Word)
对象内存结构:
- 每个对象头部包含 Mark Word(存储哈希码、GC 年龄、锁状态等)。
- 锁状态标志位:标识当前对象的锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
Mark Word 结构(以 64 位 JVM 为例):
锁状态 Mark Word 内容 无锁 哈希码 + 分代年龄 + 未使用位 偏向锁 线程ID + Epoch + 分代年龄 + 偏向锁标志 轻量级锁 指向栈中锁记录的指针 重量级锁 指向操作系统互斥量(Mutex)的指针
4. 锁升级优化(Java 6+)
为了减少锁操作的开销,JVM 采用 锁膨胀 策略,按竞争激烈程度逐步升级锁:
偏向锁(Biased Locking):
- 场景:无实际竞争,只有一个线程访问。
- 原理:在 Mark Word 中记录线程ID,后续无需 CAS 操作。
- 优化:消除无竞争下的同步开销。
轻量级锁(Lightweight Locking):
- 场景:多个线程交替执行,无并发竞争。
- 原理:
- 线程栈中创建 锁记录(Lock Record)。
- 通过 CAS 将 Mark Word 复制到锁记录,并尝试将对象头指向锁记录。
- 失败时:升级为重量级锁。
重量级锁(Heavyweight Locking):
- 场景:高并发竞争。
- 原理:依赖操作系统提供的 互斥量(Mutex),线程进入阻塞状态。
- 缺点:涉及用户态到内核态切换,开销较大。
5. 锁的获取与释放流程
6. 关键机制
可重入性:
- 同一线程可重复获取同一把锁(通过计数器实现)。
- 示例:
synchronized void a() { b(); } synchronized void b() {} // 允许重入
自旋优化:
- 轻量级锁竞争失败时,线程不会立即阻塞,而是 自旋等待(循环尝试获取锁)。
- 避免线程切换开销(适用于锁占用时间短的场景)。
适应性自旋(Java 6+):
- 根据历史自旋成功率动态调整自旋时间。
7. 与其他锁的对比
synchronized | ReentrantLock | |
---|---|---|
实现方式 | JVM 内置,自动管理 | JDK 实现,需手动释放 |
锁类型 | 非公平锁(默认) | 可配置公平/非公平 |
条件变量 | 通过 wait() /notify() | 支持多个 Condition |
性能 | 优化后接近 ReentrantLock | 高竞争下更灵活 |
8. 使用注意事项
锁对象的选择:
- 避免锁字符串常量、基本类型包装类(易引起意外竞争)。
- 推荐使用私有不可变对象(如
private final Object lock = new Object()
)。
锁粒度控制:
- 尽量缩小同步代码块范围,减少竞争。
避免死锁:
- 按固定顺序获取多把锁,或使用超时机制(如
tryLock
)。
- 按固定顺序获取多把锁,或使用超时机制(如
总结
synchronized
通过对象头中的 Mark Word 和 JVM 的 锁升级机制(偏向锁→轻量级锁→重量级锁)实现高效同步。其设计目标是 在无竞争时尽量减少开销,高竞争时保证正确性。理解其底层原理有助于编写更高效、安全的并发代码。