Synchronized锁的升级流程详解

在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锁的升级过程:

java 复制代码
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();  
    }  
}
相关推荐
鲤籽鲲2 分钟前
C# MethodTimer.Fody 使用详解
开发语言·c#·mfc
亚图跨际5 分钟前
Python和R荧光分光光度法
开发语言·python·r语言·荧光分光光度法
Rverdoser13 分钟前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
dj244294570717 分钟前
JAVA中的Lamda表达式
java·开发语言
工业3D_大熊30 分钟前
3D可视化引擎HOOPS Luminate场景图详解:形状的创建、销毁与管理
java·c++·3d·docker·c#·制造·数据可视化
szc176734 分钟前
docker 相关命令
java·docker·jenkins
程序媛-徐师姐43 分钟前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
yngsqq44 分钟前
c#使用高版本8.0步骤
java·前端·c#
流星白龙1 小时前
【C++习题】10.反转字符串中的单词 lll
开发语言·c++
尘浮生1 小时前
Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea