目录
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、锁升级过程
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 偏向锁升级:当一个线程访问同步块时,首先会尝试获取偏向锁。如果当前对象没有被其他线程竞争过,并且持有偏向锁的线程仍然存活,那么当前线程可以直接获取偏向锁,不会发生锁升级。
- 轻量级锁升级:如果获取偏向锁失败,表示当前对象存在竞争,那么偏向锁会升级为轻量级锁。这时,JVM会通过CAS操作将对象头中的锁标记改为指向线程栈中的锁记录(Lock Record)的指针,并将对象的内容复制到锁记录中。
- 自旋锁升级:如果轻量级锁获取失败,即有多个线程竞争同一个对象的锁,那么轻量级锁会升级为自旋锁。自旋锁不会使线程阻塞,而是让线程执行忙等待,尝试反复获取锁。这样可以避免线程切换带来的性能损失。
- 重量级锁升级:当自旋锁尝试获取锁的次数达到一定阈值,或者等待时间超过一定限制时,自旋锁会升级为重量级锁。重量级锁会使线程阻塞,将竞争锁的线程放入等待队列,等待锁释放后进行唤醒。
大体的升级流程图:
具体流程图如下:
5、锁是否可以降级?
在Java中,锁通常不会主动降级,也就是说,一旦锁升级到了更高级别的锁(如从偏向锁升级到轻量级锁或重量级锁),就不会再自动降级回低级别的锁。
然而,有一种情况下锁会出现降级的行为,即重量级锁在释放时可以降级为轻量级锁。这种降级发生在持有重量级锁的线程释放锁之后,如果接下来的竞争情况较为温和,即锁的争用程度较低,系统会尝试将重量级锁降级为轻量级锁,以减少后续线程竞争锁时的开销。
降级的过程是由JVM自动处理的,具体的触发条件和策略可能因JVM实现而有所不同。一般来说,当释放重量级锁的线程检测到没有其他线程争用同一个锁时,会将锁降级为轻量级锁。
需要注意的是,锁的降级并非在所有情况下都发生,它依赖于系统的竞争情况和JVM的具体实现。在实际应用中,我们无法直接控制锁的降级行为,因此在选择和使用锁时,应根据具体情况和需求综合考虑,权衡锁的级别和性能。