JAVA synchronized锁优化:偏向锁撤销与轻量级锁膨胀全流程解析
各位同学,大家好!今天我们来深入探讨Java synchronized 关键字的锁优化机制,重点分析偏向锁的撤销以及轻量级锁的膨胀全流程。synchronized 作为Java并发编程中最重要的同步手段之一,其性能至关重要。了解其底层优化原理,有助于我们编写更高效的并发程序。
1. 锁的状态与升级过程
Java的synchronized锁在JDK 1.6之后引入了锁升级的概念,旨在减少不必要的锁竞争,提高并发性能。锁的状态从低到高依次为:无锁、偏向锁、轻量级锁、重量级锁。锁升级是单向的,只能升级不能降级(但可以被 JVM 优化为锁消除或锁粗化)。
| 锁状态 | 适用场景 | 开销 | 说明 |
|---|---|---|---|
| 无锁 | 没有线程竞争资源的情况 | 极低 | 对象头中没有锁标志位,或者锁标志位为无锁状态。 |
| 偏向锁 | 只有一个线程访问同步块的情况 | 非常低 | 线程获得锁后,会将线程ID记录在对象头中,以后该线程再次进入同步块时,无需进行任何同步操作。 |
| 轻量级锁 | 少量线程竞争资源,但竞争时间很短的情况 | 较低 | 线程通过CAS尝试将对象头替换为指向锁记录的指针。如果成功,则获得锁;否则,锁膨胀为重量级锁。 |
| 重量级锁 | 多个线程竞争资源,且竞争时间较长的情况 | 高 | 线程进入阻塞状态,等待操作系统的调度。涉及到用户态和内核态的切换,开销很大。 |
2. 偏向锁
偏向锁的核心思想是:如果一个锁总是被同一个线程持有,则后续该线程进入同步块时,无需进行任何同步操作,甚至不用CAS。
2.1 偏向锁的获取
当一个线程第一次访问一个synchronized代码块时,JVM会将对象头的Mark Word设置为偏向模式,并将当前线程的ID记录在对象头中。之后,该线程再次访问该同步代码块时,只需检查对象头的Mark Word中是否包含自己的线程ID,如果包含,则认为该线程拥有锁,直接进入同步块,无需进行任何同步操作。
2.2 偏向锁的撤销
偏向锁并非总是有效,当有其他线程尝试竞争偏向锁时,偏向锁就会被撤销。撤销过程需要等到全局安全点(safepoint),暂停拥有偏向锁的线程,然后检查锁的状态。撤销场景主要有以下几种:
-
情况一:其他线程尝试竞争偏向锁
如果其他线程尝试竞争已经被偏向的锁,JVM会检查拥有偏向锁的线程是否还存活。
- 如果拥有偏向锁的线程已经死亡,则可以将对象头重置为无锁状态,允许其他线程通过CAS竞争锁。
- 如果拥有偏向锁的线程仍然存活,JVM会暂停该线程,检查该线程是否还在执行
synchronized代码块。- 如果该线程不在执行
synchronized代码块,则可以将对象头重置为无锁状态,并升级为轻量级锁,允许其他线程通过CAS竞争锁。 - 如果该线程还在执行
synchronized代码块,则将对象头升级为轻量级锁,并将拥有偏向锁的线程的栈帧中的锁记录指向该对象。
- 如果该线程不在执行
-
情况二:调用对象的
hashCode()方法如果一个对象已经处于偏向锁状态,并且调用了该对象的
hashCode()方法,那么偏向锁会被撤销,并升级为轻量级锁。这是因为对象的hashCode()方法需要存储在对象头的Mark Word中,而偏向锁也需要使用Mark Word来存储线程ID。两者冲突,所以偏向锁会被撤销。
2.3 偏向锁撤销的代码模拟
以下代码模拟了偏向锁撤销的过程。为了方便观察,这里使用Thread.sleep()模拟线程的运行状态。
import java.util.concurrent.locks.LockSupport;
public class BiasedLockRevocation {
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 预热,让对象进入可偏向状态
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
// do nothing
}
}
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 1 acquired biased lock");
try {
Thread.sleep(5000); // 模拟线程长时间持有锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 releasing biased lock");
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
// 等待Thread-1获取偏向锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 trying to acquire lock");
synchronized (lock) {
System.out.println("Thread 2 acquired lock");
}
}, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Main thread finished");
}
}
在这个例子中,Thread-1首先获取lock对象的偏向锁,并持有一段时间。Thread-2尝试获取lock对象锁,此时会触发偏向锁的撤销。具体过程如下:
Thread-1获取lock对象的偏向锁,并将自己的线程ID记录在lock对象的对象头中。Thread-2尝试获取lock对象的锁,发现对象头中记录的线程ID不是自己的,于是触发偏向锁的撤销。- JVM暂停
Thread-1,检查Thread-1是否还在执行synchronized代码块。 Thread-1还在执行synchronized代码块,JVM将lock对象的对象头升级为轻量级锁,并将Thread-1的栈帧中的锁记录指向lock对象。Thread-2尝试通过CAS获取lock对象的轻量级锁,成功后进入synchronized代码块。
3. 轻量级锁
轻量级锁适用于线程交替执行同步块的场景。其核心思想是:避免使用操作系统互斥量(Mutex),而是通过CAS操作尝试获取锁。
3.1 轻量级锁的获取
- 线程在进入同步块之前,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record),用于存储锁的信息。
- 线程尝试使用CAS操作将对象头的Mark Word替换为指向锁记录的指针。
- 如果CAS操作成功,表示线程获取锁成功,对象头的Mark Word指向线程的锁记录。
- 如果CAS操作失败,表示有其他线程正在持有锁,当前线程会尝试自旋等待一段时间,再次尝试CAS操作。如果自旋超过一定次数仍然失败,则轻量级锁会膨胀为重量级锁。
3.2 轻量级锁的膨胀
当多个线程竞争同一个锁,并且自旋超过一定次数仍然无法获取锁时,轻量级锁会膨胀为重量级锁。膨胀过程如下:
- JVM会创建一个互斥量(Mutex),并将对象头的Mark Word指向该互斥量。
- 当前线程进入阻塞状态,等待操作系统的调度。
- 当持有锁的线程释放锁时,会唤醒等待队列中的一个线程。
- 被唤醒的线程尝试获取锁,如果成功,则进入同步块。
3.3 轻量级锁膨胀的代码模拟
以下代码模拟了轻量级锁膨胀的过程。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class LightweightLockEscalation {
static Object lock = new Object();
static int counter = 0;
static final int THREAD_COUNT = 10;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
synchronized (lock) {
counter++;
}
}
latch.countDown();
});
threads.add(thread);
thread.start();
}
latch.await();
System.out.println("Counter value: " + counter);
}
}
在这个例子中,多个线程同时竞争lock对象锁。由于线程数量较多,竞争激烈,轻量级锁会很快膨胀为重量级锁。
4. 锁消除与锁粗化
除了偏向锁和轻量级锁,JVM还提供了锁消除和锁粗化两种优化手段。
4.1 锁消除
锁消除是指JVM在编译时,如果发现某些synchronized代码块中的锁实际上不会发生竞争,那么JVM会消除这些锁。
例如:
public String appendString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
synchronized (sb) {
sb.append(s1);
sb.append(s2);
}
return sb.toString();
}
在这个例子中,StringBuffer对象sb只在appendString方法中使用,不会被其他线程访问,因此synchronized (sb)实际上不会发生竞争。JVM可以消除这个锁,提高性能。
4.2 锁粗化
锁粗化是指JVM将多个相邻的synchronized代码块合并成一个更大的synchronized代码块,从而减少锁的获取和释放次数。
例如:
public void processData(Data data) {
synchronized (data) {
data.updateField1();
}
synchronized (data) {
data.updateField2();
}
synchronized (data) {
data.updateField3();
}
}
在这个例子中,有三个相邻的synchronized代码块,它们都使用同一个锁data。JVM可以将这三个synchronized代码块合并成一个更大的synchronized代码块,减少锁的获取和释放次数。
public void processData(Data data) {
synchronized (data) {
data.updateField1();
data.updateField2();
data.updateField3();
}
}
5. 总结:理解锁优化,提升并发性能
通过以上分析,我们详细了解了synchronized锁的优化机制,包括偏向锁的获取与撤销、轻量级锁的获取与膨胀,以及锁消除和锁粗化。理解这些优化原理,有助于我们编写更高效的并发程序,避免不必要的锁竞争,提升系统性能。在实际开发中,我们需要根据具体的场景选择合适的同步策略,才能充分发挥多核CPU的性能优势。