在Java多线程编程中,synchronized关键字用于确保在同一时刻只有一个线程可以访问被锁定的资源,从而维护数据的一致性和安全性。然而,在多线程环境中,锁的频繁获取和释放会带来性能开销。为了提高性能,Java虚拟机(JVM)在JDK 1.6及以后的版本中引入了锁的升级机制,通过动态调整锁的策略来减少同步操作的开销。本文将详细解释synchronized锁的升级流程,包括无锁状态、偏向锁、轻量级锁和重量级锁四种状态及其转换过程。
锁的状态
无锁状态
当一个对象刚被创建时,它处于无锁状态,此时没有线程持有锁,所有访问同步代码块的线程都是无锁竞争状态。在这种状态下,对象的Mark Word(对象头的一部分,用于存储锁状态及线程信息)没有记录任何锁信息。
偏向锁
偏向锁是为了解决单线程访问共享资源的场景而设计的。当一个线程首次获得对象锁时,JVM会将锁设置为偏向锁,并将锁对象的Mark Word中的线程ID设置为当前线程的ID。后续当这个线程再次请求相同的锁时,只需检查Mark Word中的线程ID是否与当前线程ID一致。如果一致,说明还是原来的线程持有锁,可以直接进入同步代码块,无需进行额外的同步操作。偏向锁减少了轻量级锁中CAS操作的开销,提高了性能。
轻量级锁
当有第二个线程尝试获取已被偏向锁锁定的对象时,偏向锁失效,JVM会尝试升级为轻量级锁。线程会在当前线程栈中创建一个锁记录(Lock Record),并将锁对象的Mark Word替换为指向锁记录的指针,同时在锁记录中存储当前线程的ID和一个指向原Mark Word副本的指针。使用CAS操作尝试将锁对象的Mark Word设置为指向锁记录的指针。如果成功,线程获得轻量级锁并执行同步代码;如果失败(即有其他线程已持有锁),则进入下一步骤。轻量级锁适用于短暂的、低竞争的同步场景,通过自旋等待和CAS操作避免了线程切换的开销。
重量级锁
当自旋尝试失败或自旋超过一定阈值,或者系统检测到多个线程长期竞争同一锁时,轻量级锁会升级为重量级锁。重量级锁通常涉及操作系统级别的互斥量(Mutex),线程在无法获得锁时会被挂起,不再消耗CPU资源,直到持有锁的线程释放锁后,操作系统再唤醒等待队列中的下一个线程。重量级锁提供了严格的互斥保证,适用于高竞争或锁占用时间较长的场景,虽然开销较大,但能有效防止过多线程同时阻塞在自旋状态。
Mark Word
Mark Word是对象头中最重要的部分,它是一个特殊的字段,用于存储对象的元数据信息,包括锁状态和线程信息。在64位JVM中,Mark Word占用64位。当一个共享资源首次被某个线程访问时,锁就会从无锁状态升级到偏向锁状态,偏向锁会在Mark Word的偏向线程ID里存储当前线程的操作系统线程ID,偏向锁标识位是1,锁标识位是01。此后如果当前线程再次进入临界区域时,只比较这个偏向线程ID即可。
锁升级流程
无锁状态到偏向锁
当一个对象首次被某个线程访问时,它处于无锁状态。当第一个线程访问同步代码块或方法时,JVM会将对象头的Mark Word设置为偏向锁,并记录这个线程的ID。此时,如果后续的访问仍然是由这个线程发起的,无需进行同步操作,直接执行代码即可,因为锁已经偏向于这个线程。
偏向锁到轻量级锁
如果有其他线程尝试访问这个同步块,偏向锁将被撤销,并进入轻量级锁状态。撤销时会有一定的开销,包括检查偏向锁标识、CAS操作尝试清除偏向锁等。当有第二个线程尝试获取锁时,偏向锁被撤销,转换为轻量级锁。线程会在自己的栈帧中创建一个称为Lock Record的空间,用于存储锁的Mark Word的拷贝。然后通过CAS操作尝试将对象头的Mark Word替换为指向Lock Record的指针。如果成功,线程获得锁;失败,则说明存在竞争,线程将自旋一段时间,不断尝试CAS操作直到成功或达到自旋上限。
轻量级锁到重量级锁
如果自旋超过一定次数(自旋阈值)仍未获得锁,轻量级锁将升级为重量级锁。此时,JVM会调用操作系统的互斥量(mutex)来实现线程阻塞和唤醒,这会导致线程挂起和恢复,开销较大。未获取到锁的线程会被阻塞,进入等待队列,而持有锁的线程执行完毕后,会唤醒队列中的下一个等待线程。
锁升级过程中的关键概念
CAS操作
CAS(Compare and Swap)操作是一种无锁算法,用于在多线程环境下实现原子操作。它比较内存中的值与预期值是否相等,如果相等则更新为新值,否则不做任何操作。在轻量级锁的获取过程中,CAS操作用于尝试将对象头的Mark Word替换为指向Lock Record的指针。
自旋锁
自旋锁是一种轻量级的锁机制,当线程尝试获取锁失败时,它不会立即被阻塞,而是会在一个循环中不断尝试获取锁,直到成功或达到某个条件(如自旋次数上限)。自旋锁避免了线程切换的开销,但在高竞争场景下可能会导致CPU资源的浪费。
自适应自旋
自适应自旋的基本思想是根据锁的争用情况,决定线程是否应该自旋等待,以及自旋等待的时间。JVM会根据历史数据动态调整自旋的次数,以减少不必要的自旋开销。
锁升级的意义
锁的升级过程是为了提高多线程环境下的性能和吞吐量,减少同步操作的开销,并尽量避免线程切换的开销。在大多数情况下,锁是由单个线程持有的,如果直接使用重量级锁,会浪费资源。因此,JVM根据线程竞争的情况和锁的使用情况自动进行锁的升级和降级,以优化多线程程序的性能。
代码示例
以下是一个简单的代码示例,展示了synchronized锁的升级过程:
public class SynchronizedExample {
// 对象锁示例
public synchronized void method1() {
System.out.println("Method 1 executing by " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作,增加锁竞争的可能性
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 代码块锁示例,锁定的是object实例
private Object object = new Object();
public void method2() {
synchronized (object) { // 这里使用的是对象锁
System.out.println("Method 2 executing by " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Thread t1 = new Thread(() -> example.method1(), "Thread-1");
Thread t2 = new Thread(() -> example.method2(), "Thread-2");
t1.start();
t2.start();
}
}