synchronized全解析:从锁升级到性能优化,彻底掌握Java内置锁
作为Java中最常用的同步机制,synchronized背后的实现原理和优化策略值得深入理解。本文将从底层实现到高级特性,全面解析synchronized的锁机制。
文章目录
- 1. synchronized实现原理揭秘
- 1.1 同步的基本概念
- 1.2 Monitor机制核心原理
- 1.3 字节码层面分析
- 2. 锁的膨胀升级全过程
- 2.1 对象头与锁状态
- 2.2 完整的锁升级流程
- 3. synchronized如何保证三大特性
- 3.1 原子性(Atomicity)
- 3.2 可见性(Visibility)
- 3.3 有序性(Ordering)
- 4. synchronized vs ReentrantLock深度对比
- 4.1 特性对比表格
- 4.2 使用场景对比
- 5. 重量级锁的价值与代价
- 5.1 为什么需要重量级锁?
- 5.2 性能权衡分析
- 6. 锁优化技术详解
- 6.1 锁粗化(Lock Coarsening)
- 6.2 锁消除(Lock Elimination)
- 6.3 自适应自旋(Adaptive Spinning)
- 7. 常见问题深度解答
- 7.1 synchronized的锁升级过程有几次自旋?
- 7.2 synchronized锁的是什么?
- 7.3 synchronized的锁能降级吗?
- 7.4 synchronized是非公平锁吗?如何体现?
1. synchronized实现原理揭秘
1.1 同步的基本概念
在多线程编程中,当多个线程同时访问共享、可变的临界资源时,需要采用同步机制来保证线程安全。synchronized作为Java内置的同步机制,通过对临界资源进行序列化访问来解决并发问题。
1.2 Monitor机制核心原理
synchronized基于JVM的Monitor(监视器锁) 实现,每个Java对象都关联一个Monitor对象:
// HotSpot虚拟机中ObjectMonitor的核心数据结构(C++实现)
ObjectMonitor() {_count = 0; // 记录线程进入锁的次数_owner = NULL; // 指向持有锁的线程_WaitSet = NULL; // 处于wait状态的线程队列_EntryList = NULL; // 处于等待锁block状态的线程队列_recursions = 0; // 锁的重入次数
}
Monitor工作流程:
- 线程进入同步代码时,尝试通过
monitorenter指令获取Monitor所有权 - 获取成功则设置_owner为当前线程,_count计数器加1
- 如果获取失败,线程进入_EntryList等待
- 线程执行完同步代码后,通过
monitorexit指令释放锁,_count减1
1.3 字节码层面分析
同步代码块被编译为字节码后,会生成对应的monitorenter和monitorexit指令:
public void syncMethod() {synchronized(this) {System.out.println("同步代码块");}
}// 对应的字节码:
// monitorenter // 进入同步块
// ... 业务逻辑代码
// monitorexit // 正常退出
// monitorexit // 异常退出 - 保证在异常情况下也能释放锁
而同步方法则通过方法访问标志ACC_SYNCHRONIZED来实现:
public synchronized void syncMethod() {System.out.println("同步方法");
}// 方法flags中包含ACC_SYNCHRONIZED标志
2. 锁的膨胀升级全过程
2.1 对象头与锁状态
锁状态信息存储在对象的Mark Word中,HotSpot虚拟机的对象内存布局如下:
32位虚拟机的Mark Word结构:
| 锁状态 | 25bit | 4bit | 1bit | 2bit |
|---|---|---|---|---|
| 无锁态 | 对象的hashCode | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID + Epoch | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向Monitor的指针 | 10 | ||
| GC标记 | 空 | 11 |
2.2 完整的锁升级流程
步骤1:无锁 → 偏向锁
- 当第一个线程访问同步块时,检查对象Mark Word中的偏向锁标识
- 如果支持偏向锁(默认开启),CAS操作将线程ID记录到Mark Word
- 成功则进入偏向模式,后续该线程进入同步块无需同步操作
步骤2:偏向锁 → 轻量级锁
- 当另一个线程尝试获取锁时,检查持有偏向锁的线程是否存活
- 如果原线程已不存活,撤销偏向锁到无锁状态,重新竞争
- 如果原线程仍存活,检查是否还需要继续持有锁
- 发生竞争时,偏向锁升级为轻量级锁
步骤3:轻量级锁 → 重量级锁
- 轻量级锁通过CAS自旋尝试获取锁
- 如果自旋超过一定次数(JDK6之前默认10次,JDK6引入自适应自旋)仍未获取到锁
- 或者有第三个线程参与竞争,轻量级锁升级为重量级锁
3. synchronized如何保证三大特性
3.1 原子性(Atomicity)
synchronized通过Monitor的互斥特性保证原子性:
public class AtomicExample {private int count = 0;private final Object lock = new Object();public void increment() {synchronized(lock) {count++; // 这个操作在同步块内是原子的}}
}
原理分析:
monitorenter和monitorexit指令确保同步块内的所有操作作为一个不可分割的整体执行- 同一时刻只有一个线程能够持有Monitor锁
3.2 可见性(Visibility)
synchronized通过内存屏障和happens-before原则保证可见性:
public class VisibilityExample {private boolean flag = false;private final Object lock = new Object();public void writer() {synchronized(lock) {flag = true; // 对后续获取同一个锁的线程可见}}public void reader() {synchronized(lock) {if(flag) {// 一定能看到writer线程的修改System.out.println("Flag is true");}}}
}
实现机制:
- 线程释放锁时,JMM强制将工作内存中的修改刷新到主内存
- 线程获取锁时,JMM使该线程的工作内存无效,从主内存重新加载变量
3.3 有序性(Ordering)
synchronized通过限制指令重排序来保证有序性:
public class OrderingExample {private int a = 0;private boolean initialized = false;private final Object lock = new Object();public void init() {synchronized(lock) {a = 1; // 不会重排序到initialized = true之后initialized = true;}}
}
有序性保证:
- 同步块内的指令不会重排序到同步块之外
- 不同线程按照获取锁的顺序执行同步代码,建立执行顺序的约束
4. synchronized vs ReentrantLock深度对比
4.1 特性对比表格
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现机制 | JVM内置,基于Monitor | JDK实现,基于AQS |
| 锁的获取 | 隐式获取释放 | 显式lock()/unlock() |
| 可中断性 | 不支持 | 支持lockInterruptibly() |
| 超时机制 | 不支持 | 支持tryLock(timeout) |
| 公平性 | 非公平锁 | 可选择公平/非公平 |
| 条件变量 | 一个Condition | 多个Condition |
| 性能 | JDK6后优化,与ReentrantLock相当 | 稳定高效 |
| 代码复杂度 | 简单,自动释放锁 | 复杂,需要手动释放 |
4.2 使用场景对比
synchronized适用场景:
// 简单的同步需求,代码简洁
public class SimpleCounter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}
ReentrantLock适用场景:
// 复杂的同步需求,需要高级特性
public class AdvancedCounter {private int count = 0;private final ReentrantLock lock = new ReentrantLock(true); // 公平锁private final Condition notZero = lock.newCondition();public void increment() {lock.lock();try {count++;notZero.signalAll(); // 精确唤醒等待条件线程} finally {lock.unlock();}}
}
5. 重量级锁的价值与代价
5.1 为什么需要重量级锁?
尽管重量级锁性能较低,但在高竞争场景下仍然必要:
轻量级锁的局限性:
- 自旋锁消耗CPU资源,长时间自旋得不偿失
- 多个线程竞争时,CAS操作成功率急剧下降
- 线程数超过CPU核心数时,自旋策略失效
重量级锁的优势:
// 高竞争场景下,重量级锁通过线程挂起避免CPU空转
public class HighContentionExample {private final Object lock = new Object();public void highContentionMethod() {synchronized(lock) {// 在100个线程竞争的场景下// 重量级锁通过线程排队,避免99个线程空转消耗CPUdoSomething();}}
}
5.2 性能权衡分析
低竞争场景性能对比:
- 偏向锁/轻量级锁:CAS操作,用户态完成,性能极高
- 重量级锁:系统调用,用户态/内核态切换,性能较低
高竞争场景性能对比:
- 偏向锁/轻量级锁:大量CAS失败和自旋,CPU资源浪费
- 重量级锁:线程挂起等待,CPU资源有效利用
6. 锁优化技术详解
6.1 锁粗化(Lock Coarsening)
// 优化前:多次锁申请释放
public void beforeOptimization() {for(int i = 0; i < 1000; i++) {synchronized(lock) {// 少量操作}}
}// 优化后:一次锁申请释放
public void afterOptimization() {synchronized(lock) {for(int i = 0; i < 1000; i++) {// 合并后的操作}}
}
6.2 锁消除(Lock Elimination)
基于逃逸分析的锁优化:
// 这个StringBuffer不会被其他线程访问,JVM会消除锁
public String concat(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1); // append是同步方法,但锁会被消除sb.append(s2);sb.append(s3);return sb.toString();
}
开启锁消除参数:-XX:+EliminateLocks
6.3 自适应自旋(Adaptive Spinning)
JDK6引入的自适应自旋优化:
- 根据之前自旋的成功率动态调整自旋次数
- 如果之前自旋很少成功,则减少自旋次数
- 如果之前自旋经常成功,则增加自旋次数
7. 常见问题深度解答
7.1 synchronized的锁升级过程有几次自旋?
在轻量级锁阶段,线程会进行自旋尝试获取锁。JDK6之前自旋次数固定(默认10次),JDK6引入自适应自旋:
- 自旋次数不再固定,由JVM根据监控数据动态决定
- 如果之前自旋成功获取锁,则增加自旋次数
- 如果很少自旋成功,则可能直接升级为重量级锁
7.2 synchronized锁的是什么?
synchronized锁的是对象,而不是代码或引用:
public class LockTargetExample {private final Object lock1 = new Object();private final Object lock2 = new Object();public void method1() {synchronized(lock1) { // 锁的是lock1对象// ...}}public void method2() {synchronized(lock2) { // 锁的是lock2对象,与method1不互斥// ...}}
}
7.3 synchronized的锁能降级吗?
不能降级。锁升级是单向过程:
- 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 设计初衷是为了优化性能,降级带来的收益有限且实现复杂
7.4 synchronized是非公平锁吗?如何体现?
是的,synchronized是非公平锁,体现在:
public class FairnessExample {private final Object lock = new Object();public void demonstrateUnfairness() {// 线程A、B、C都在等待锁// 线程A释放锁后,新来的线程D可能比等待中的B、C先获取到锁synchronized(lock) {// 新线程可以"插队"获取锁}}
}
非公平锁的优势:
- 减少线程切换开销,提高吞吐量
- 避免线程唤醒的延迟
