【JUC进阶】09. 关于锁升级

目录

1、前言

2、回顾

2.1、对象头和内存布局

2.2、四大锁回顾

3、状态转换

3.1、锁状态

3.1.1、无锁状态

3.1.2、偏向锁状态

3.1.3、轻量级锁状态

3.1.4、重量级锁状态

3.2、状态转换条件

3.2.1、无锁 -> 偏向锁

3.2.2、偏向锁 -> 无锁

3.2.3、偏向锁 -> 轻量级锁

3.2.4、轻量级锁 -> 重量级锁

3.2.5、重量级锁 -> 轻量级锁

4、锁升级过程

5、锁是否可以降级?


1、前言

在并发编程中,锁是保证线程安全的重要机制。然而,传统的锁在高并发场景下性能可能受到限制。为了解决这个问题,JUC引入了锁升级的概念,通过在运行时动态调整锁的状态,提升并发性能。前面我们分别介绍了无锁,偏向锁,轻量级锁,自旋锁,重量级锁的知识。这些其实就是JUC中对锁的优化而会转换的几种状态,也就是我们经常听到的锁升级。

2、回顾

2.1、对象头和内存布局

对这一块知识还不太理解的,可以翻看《【JUC进阶】03. Java对象头和内存布局》。这边简单回顾一下。

Java对象在堆内存中存储的布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。而对象头(Header)中包含了两部分信息:标记字段(Mark Word)和Class对象指针(Class Pointer)。

其中,标记字段(Mark Word)用于存储对象自身的运行时数据,如HashCode(哈希码)、GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。Class对象指针(Class Pointer)则是对象指向它的类型元数据的指针,JVM通过这个指针来确定该对象是哪个类的实例。

当对象持有各种级别锁的时候,标记字段(Mark Word)中会存储相关的标志位,线程ID等信息。

2.2、四大锁回顾

前面我们分别详细介绍了几种锁的知识。这里将几种锁的相关特性进行汇总。

锁类型

特性

本质

原理

优点

缺点

使用场景

性能开销

无锁

无阻塞,无同步

通过CAS实现原子操作

使用原子操作实现并发控制

无阻塞,避免线程阻塞和切换的开销

自旋等待消耗CPU资源

并发度高,争用少的情况

较低,仅涉及原子操作的性能损耗

偏向锁

适用于单线程

通过线程ID标识持有者

初次获取锁时,将线程ID记录在锁对象的Mark Word

避免了多线程竞争,加速单线程执行路径

多线程竞争时会撤销偏向锁,引入额外开销

频繁获取锁的单线程

较低,只涉及线程ID的比较和写操作

轻量级锁

自旋等待

通过CAS和自旋实现

偏向锁撤销或多线程竞争时,使用CAS将Mark Word 替换

减少了线程阻塞和切换的开销,适用于短时间的锁竞争

自旋等待消耗CPU资源

短时间的锁竞争

中等,涉及CAS操作和自旋等待

重量级锁

阻塞

使用操作系统Mutex

线程竞争激烈时,使用操作系统提供的互斥机制

可以有效解决多线程竞争,保证数据的安全和正确性

需要进行线程阻塞和切换,开销较大

长时间的锁竞争,保证数据的安全和正确性

高,涉及线程阻塞、切换和操作系统调度

可见,相对性能开销而言:无锁 ≤ 偏向锁 ≤ 轻量级锁 ≤ 重量级锁。如果有一定编程经验的朋友,一定会有这样的意识,升级过程必然会影响性能的开销,所以按照性能开销的分布是否可以推导出锁升级(状态转换)的过程。答案是必然的。

3、状态转换

3.1、锁状态

3.1.1、无锁状态

在无锁状态下,线程可以自由地访问共享资源,没有任何锁的限制和竞争。当多个线程同时访问同一个共享资源时,会发生数据竞争和线程安全问题。

3.1.2、偏向锁状态

当只有一个线程访问同步代码块时,JVM会将对象标记为偏向锁状态。偏向锁的目的是减少无竞争情况下的锁开销。当线程第一次进入同步代码块时,JVM会将对象头中的线程ID记录为当前线程的ID,并将对象头的状态设置为偏向锁。之后,该线程再次进入同步代码块时,无需进行额外的同步操作,直接进入同步状态。

3.1.3、轻量级锁状态

当多个线程之间存在轻度竞争时,JVM会将对象标记为轻量级锁状态。轻量级锁的目的是在减少线程切换和锁撤销开销的前提下,提供一种低竞争的同步机制。

3.1.4、重量级锁状态

当多个线程之间存在激烈竞争时,JVM会将对象标记为重量级锁状态。重量级锁使用操作系统提供的互斥量实现,涉及到线程的阻塞和唤醒,需要操作系统的介入。

3.2、状态转换条件

3.2.1、无锁 -> 偏向锁

  • 当一个线程第一次访问同步代码块时,对象会被标记为偏向锁状态,并记录当前线程的ID。
  • 转换条件:无锁状态下的对象被另一个线程访问。

3.2.2、偏向锁 -> 无锁

  • 当对象处于偏向锁状态时,如果另一个线程尝试获取锁,偏向锁会被撤销。
  • 转换条件:另一个线程尝试获取偏向锁。

3.2.3、偏向锁 -> 轻量级锁

  • 当一个线程反复进入同步代码块,但存在竞争时,偏向锁会升级为轻量级锁。
  • 转换条件:同一个对象上的偏向锁存在竞争。

3.2.4、轻量级锁 -> 重量级锁

  • 当多个线程之间存在激烈竞争时,轻量级锁会升级为重量级锁。
  • 转换条件:轻量级锁的CAS操作竞争失败。

3.2.5、重量级锁 -> 轻量级锁

  • 当持有重量级锁的线程释放锁时,锁会尝试降级为轻量级锁。
  • 转换条件:持有重量级锁的线程释放锁。

4、锁升级过程

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
  1. 偏向锁升级:当一个线程访问同步块时,首先会尝试获取偏向锁。如果当前对象没有被其他线程竞争过,并且持有偏向锁的线程仍然存活,那么当前线程可以直接获取偏向锁,不会发生锁升级。
  2. 轻量级锁升级:如果获取偏向锁失败,表示当前对象存在竞争,那么偏向锁会升级为轻量级锁。这时,JVM会通过CAS操作将对象头中的锁标记改为指向线程栈中的锁记录(Lock Record)的指针,并将对象的内容复制到锁记录中。
  3. 自旋锁升级:如果轻量级锁获取失败,即有多个线程竞争同一个对象的锁,那么轻量级锁会升级为自旋锁。自旋锁不会使线程阻塞,而是让线程执行忙等待,尝试反复获取锁。这样可以避免线程切换带来的性能损失。
  4. 重量级锁升级:当自旋锁尝试获取锁的次数达到一定阈值,或者等待时间超过一定限制时,自旋锁会升级为重量级锁。重量级锁会使线程阻塞,将竞争锁的线程放入等待队列,等待锁释放后进行唤醒。

大体的升级流程图:

具体流程图如下:

5、锁是否可以降级?

在Java中,锁通常不会主动降级,也就是说,一旦锁升级到了更高级别的锁(如从偏向锁升级到轻量级锁或重量级锁),就不会再自动降级回低级别的锁。

然而,有一种情况下锁会出现降级的行为,即重量级锁在释放时可以降级为轻量级锁。这种降级发生在持有重量级锁的线程释放锁之后,如果接下来的竞争情况较为温和,即锁的争用程度较低,系统会尝试将重量级锁降级为轻量级锁,以减少后续线程竞争锁时的开销。

降级的过程是由JVM自动处理的,具体的触发条件和策略可能因JVM实现而有所不同。一般来说,当释放重量级锁的线程检测到没有其他线程争用同一个锁时,会将锁降级为轻量级锁。

需要注意的是,锁的降级并非在所有情况下都发生,它依赖于系统的竞争情况和JVM的具体实现。在实际应用中,我们无法直接控制锁的降级行为,因此在选择和使用锁时,应根据具体情况和需求综合考虑,权衡锁的级别和性能。

猜你喜欢

转载自blog.csdn.net/p793049488/article/details/131513512