synchronized
是 Java 中用于实现线程同步的关键字,但在早期版本中,它的性能较差,主要是因为锁的实现是基于操作系统的互斥量(Mutex),会导致线程频繁地在用户态和内核态之间切换。为了提升性能,JVM 对 synchronized
进行了多种优化,主要包括 锁升级、锁消除、锁粗化 和 自适应自旋锁 等。以下是这些优化的详细说明:
1. 锁升级(锁膨胀)
锁升级是 JVM 对 synchronized
锁的优化机制,根据锁的竞争情况动态调整锁的级别,从低开销的锁逐步升级为高开销的锁。锁升级分为以下几个阶段:
(1)无锁状态
- 对象刚创建时,没有任何线程持有锁。
(2)偏向锁(Biased Locking)
- 适用场景:只有一个线程访问同步块。
- 实现原理:
- 在对象头中记录线程 ID,表示该线程持有锁。
- 如果同一线程再次访问同步块,无需加锁,直接进入。
- 优点:减少无竞争情况下的锁开销。
(3)轻量级锁(Lightweight Locking)
- 适用场景:多个线程交替访问同步块,但没有竞争。
- 实现原理:
- 使用 CAS(Compare-And-Swap)操作将对象头替换为指向线程栈中锁记录的指针。
- 如果 CAS 成功,表示获取锁;如果失败,表示有竞争,升级为重量级锁。
- 优点:减少线程阻塞和上下文切换的开销。
(4)重量级锁(Heavyweight Locking)
- 适用场景:多个线程竞争同一锁。
- 实现原理:
- 使用操作系统的互斥量(Mutex)实现锁。
- 未获取锁的线程会被阻塞,进入等待队列。
- 优点:保证线程安全,但开销较大。
2. 锁消除(Lock Elimination)
锁消除是 JVM 在编译时对代码进行优化,去除不必要的锁。
(1)适用场景
- 锁对象是局部变量,且不会被其他线程访问。
- 锁保护的代码块没有共享数据竞争。
(2)实现原理
- JVM 通过逃逸分析(Escape Analysis)判断锁对象是否可能被其他线程访问。
- 如果锁对象不会逃逸出当前方法,JVM 会消除锁。
(3)示例
public void method() {
Object lock = new Object(); // 局部变量,不会被其他线程访问
synchronized (lock) { // 锁会被消除
System.out.println("Hello");
}
}
3. 锁粗化(Lock Coarsening)
锁粗化是 JVM 将多个连续的锁操作合并为一个更大的锁操作,减少锁的获取和释放次数。
(1)适用场景
- 在循环或频繁的锁操作中,锁的获取和释放会导致性能下降。
(2)实现原理
- JVM 检测到连续的锁操作时,会将它们合并为一个更大的锁操作。
(3)示例
public void method() {
synchronized (this) {
System.out.println("Operation 1");
}
synchronized (this) {
System.out.println("Operation 2");
}
// 锁粗化后:
synchronized (this) {
System.out.println("Operation 1");
System.out.println("Operation 2");
}
}
4. 自适应自旋锁(Adaptive Spinning)
自旋锁是线程在获取锁失败时,不立即阻塞,而是循环尝试获取锁。自适应自旋锁会根据历史数据动态调整自旋次数。
(1)适用场景
- 锁竞争不激烈,且线程持有锁的时间较短。
(2)实现原理
- JVM 根据锁的历史获取情况,动态调整自旋次数。
- 如果锁经常被成功获取,则增加自旋次数;否则,减少自旋次数。
(3)优点
- 减少线程阻塞和上下文切换的开销。
5. 偏向锁撤销(Biased Lock Revocation)
当偏向锁的持有线程不再访问同步块时,JVM 会撤销偏向锁,恢复到无锁状态或升级为轻量级锁。
(1)适用场景
- 偏向锁的持有线程长时间未访问同步块。
(2)实现原理
- JVM 检测到偏向锁的持有线程不再活跃时,撤销偏向锁。
6. 总结
JVM 对 synchronized
的锁优化主要包括:
- 锁升级:根据锁的竞争情况,从偏向锁升级到轻量级锁,再到重量级锁。
- 锁消除:通过逃逸分析去除不必要的锁。
- 锁粗化:将多个连续的锁操作合并为一个更大的锁操作。
- 自适应自旋锁:根据历史数据动态调整自旋次数。
- 偏向锁撤销:当偏向锁不再需要时,撤销偏向锁。
这些优化机制显著提升了 synchronized
的性能,使其在高并发场景下仍然能够高效运行。