知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!
synchronized 深度解析
synchronized 是 Java 中实现线程同步的核心机制,通过内置的监视器锁(Monitor)保证代码块或方法的原子性、可见性和有序性。以下从底层实现、锁优化、使用场景及常见问题等方面进行详细分析。
一、底层实现原理
1. 对象头与 Mark Word
- 对象内存结构:
每个 Java 对象在堆中存储时,其对象头包含两部分:- Mark Word(标记字段):存储对象的哈希码、锁状态、GC 分代年龄等。
- Klass Pointer(类型指针):指向类的元数据。
- Mark Word 结构(以 64 位 JVM 为例):
锁状态 存储内容 无锁 对象哈希码、分代年龄、是否偏向锁(0)等。 偏向锁 偏向线程 ID、偏向时间戳、分代年龄、锁标志位(01)。 轻量级锁 指向栈中锁记录的指针(Lock Record),锁标志位(00)。 重量级锁 指向监视器(Monitor)的指针,锁标志位(10)。 GC 标记 标记对象是否被垃圾回收,锁标志位(11)。
2. 监视器锁(Monitor)
-
Monitor 结构:
每个对象关联一个 Monitor,包含以下关键字段:- Owner:持有锁的线程。
- EntryList:等待获取锁的阻塞线程队列。
- WaitSet:调用
wait()后进入等待状态的线程队列。
-
锁获取流程:
- 线程通过 CAS 操作尝试将 Mark Word 指向自己的 Lock Record。
- 成功则获得轻量级锁,失败则升级为重量级锁,进入阻塞状态。
3. JVM 指令
monitorenter:进入同步代码块,尝试获取锁。monitorexit:退出同步代码块,释放锁。public void syncMethod() { synchronized (this) { // monitorenter // 临界区代码 } // monitorexit }
二、锁升级过程
Java 6 后引入锁升级机制,减少锁操作的开销:
-
偏向锁设置
- 线程首次访问对象时,JVM将线程ID写入Mark Word,进入偏向锁模式。
- 后续同一线程访问时,直接通过线程ID验证,无需CAS操作。
-
偏向锁撤销
- 当其他线程(ThreadB)尝试获取锁时,JVM检测到偏向锁持有者(ThreadA)未释放,触发偏向锁撤销。
- Mark Word升级为轻量级锁,记录Lock Record指针。
-
轻量级锁竞争
- 线程通过CAS操作竞争Lock Record指针,失败线程(ThreadB)进入自旋。
- 自旋超过阈值后,触发锁膨胀(Inflation),升级为重量级锁。
-
重量级锁
- JVM向操作系统申请Monitor对象,Mark Word指向Monitor。
- 竞争失败的线程(ThreadB)进入阻塞队列(EntryList),由操作系统调度唤醒。
锁升级流程图
无锁(001)
│
▼
偏向锁(101)───竞争发生──→ 撤销偏向锁
│ │
▼ ▼
轻量级锁(00)──CAS失败多次─→ 重量级锁(10)
锁升级的意义
- 性能优化:
根据竞争激烈程度动态调整锁机制,避免无竞争时的性能浪费(偏向锁)和过度竞争时的自旋开销(轻量级锁→重量级锁)。 - 资源节省:
仅在必要时使用重量级锁(依赖操作系统互斥量),减少内核态切换开销。
三、synchronized 的使用方式
1. 修饰实例方法
- 锁对象:当前实例(
this)。public synchronized void instanceMethod() { // 临界区代码 }
2. 修饰静态方法
- 锁对象:类的
Class对象。public static synchronized void staticMethod() { // 临界区代码 }
3. 修饰代码块
- 锁对象:显式指定的任意对象。
private final Object lock = new Object(); public void blockMethod() { synchronized (lock) { // 临界区代码 } }
四、锁优化技术
1. 锁消除(Lock Elimination)
- 原理:
JVM 通过逃逸分析,判断锁对象不会逃逸出当前线程,则直接消除锁。 - 示例:
JVM 会自动消除public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); // 对象未逃逸 sb.append(s1); sb.append(s2); return sb.toString(); }StringBuffer内部的同步操作。
2. 锁粗化(Lock Coarsening)
- 原理:
将连续的多个锁操作合并为单个锁,减少锁获取/释放次数。// 优化前 for (int i = 0; i < 100; i++) { synchronized (lock) { // 操作 } } // 优化后 synchronized (lock) { for (int i = 0; i < 100; i++) { // 操作 } }
3. 适应性自旋(Adaptive Spinning)
- 原理:
线程在竞争锁时,先自旋(循环尝试)而非直接阻塞,避免上下文切换。 - 自旋次数:
JVM 根据历史竞争情况动态调整自旋次数。
五、synchronized 的特性
1. 可重入性(Reentrancy)
- 原理:
同一线程可多次获取同一把锁,通过 Monitor 的计数器实现。public synchronized void methodA() { methodB(); // 可重入 } public synchronized void methodB() { // ... }
2. 可见性与有序性
- 可见性:
锁释放前会将工作内存中的修改刷新到主内存。 - 有序性:
通过锁保证临界区内的代码按顺序执行(as-if-serial 语义)。
六、常见问题与解决方案
1. 锁竞争激烈
- 现象:
线程频繁阻塞,CPU 利用率低。 - 解决:
- 减少锁粒度(如分段锁)。
- 使用无锁数据结构(如
ConcurrentHashMap)。
2. 死锁
- 示例:
// 线程1 synchronized (A) { synchronized (B) { ... } } // 线程2 synchronized (B) { synchronized (A) { ... } } - 解决:
- 按固定顺序获取锁。
- 使用
tryLock()设置超时。
3. 误用锁对象
- 错误示例:
private Integer lock = 1; public void syncMethod() { synchronized (lock) { // 错误!Integer 对象可能变化(自动装箱) // ... } } - 解决:
使用final修饰锁对象,避免引用变化。
七、synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM 内置,自动释放锁 | JDK 实现,需手动 lock()/unlock() |
| 可中断 | 不支持 | 支持 lockInterruptibly() |
| 公平锁 | 非公平 | 可配置公平/非公平 |
| 条件变量 | 通过 wait()/notify() | 支持多个 Condition |
| 性能 | Java 6 后优化,高竞争下略逊 | 高竞争时表现更优 |
八、示例:双重检查锁定(DCL)
public class Singleton {
private volatile static Singleton instance; // volatile 防止指令重排序
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 非原子操作需 volatile 保证可见性
}
}
}
return instance;
}
}
- 关键点:
volatile防止指令重排序导致其他线程访问到未初始化的对象。
总结
synchronized 通过锁升级机制在大多数场景下提供了高效的同步支持,但其性能仍受锁竞争程度影响。开发者需结合具体场景选择同步策略,必要时使用工具(如 jstack、JFR)监控锁状态,优化高并发程序的性能。