💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

【并发编程进阶3】锁升级_无锁

  • 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
  • 导航
  • 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
  • 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
  • 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
  • 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
  • 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨


博客目录

  • 1.描述下锁分类?
  • 2.描述下锁升级?
  • 3.偏向锁的原理?
  • 4.JVM 偏向锁可以关闭吗?
  • 5.偏向锁撤销原理?
  • 6.批量重偏向和批量撤销?
  • 7.轻量级锁加锁和解锁的过程?
  • 8.Mark Word 说明
  • 9.Mark Word 中的 epoch
  • 10.synchronized 锁信息在对象中存储?
  • 11.偏向锁和轻量级锁区别?
  • 12.描述下锁升级的过程?
  • 13.比较三种锁的优缺点及使用场景?
  • 14.为什么要引入轻量级锁?
  • 15.什么是适应性自旋?


1.描述下锁分类?

锁的类型

锁的标志

non-biasable

01 (偏向标志位为 0)

biasable

01 (偏向标志位为 1)

biased

01 (偏向标志位为 1)

thin lock

00

fat lock

10

GC

11

需要注意的是,标志的描述中有一些错误。biasablebiased都使用相同的标志,即偏向标志位为 1,而非 0。此外,GC 标志通常不用于描述锁的状态,而是与垃圾回收相关。

因此,正确的锁类型和标志的描述应为:

  • non-biasable(无锁且不可偏向): 01 (偏向标志位为 0)
  • biasable(无锁可偏向): 01 (偏向标志位为 1)
  • biased(偏向锁): 01 (偏向标志位为 1)
  • thin lock(轻量级锁): 00
  • fat lock(重量级锁): 10

以下是 Java 中常见的锁的标志以及对应的特点:

锁的类型

特点

锁标识

偏向标志位

无锁

无锁是指多个线程可以同时访问同一个变量或资源,而不需要进行同步控制,也不会发生冲突。无锁通常适用于读多写少的场景,可以提高并发性能。

01

0

偏向锁

偏向锁是一种优化手段,用于减少无竞争情况下的锁操作,从而提高性能。偏向锁会在对象头中记录拥有锁的线程 ID,当一个线程进入同步块时,如果该对象的锁状态为无锁状态,那么当前线程会尝试获取锁并将对象头中的线程ID设置为自己的 ID。如果当前线程已经拥有了该对象的锁,那么它可以直接进入同步块执行,无需再次获取锁。

01

1

轻量级锁

轻量级锁是一种基于自旋的锁,用于减少线程的上下文切换和线程阻塞、唤醒的开销。轻量级锁的实现基于对象头中的标志位,当一个线程进入同步块时,如果该对象的锁状态为无锁状态,那么当前线程会尝试使用 CAS 操作将对象头中的标志位设置为轻量级锁,并将锁的拥有者线程 ID 记录在锁记录(Lock Record)中。如果当前线程已经拥有了该对象的锁,那么它可以直接进入同步块执行,无需再次获取锁。如果锁操作失败,那么当前线程会进入自旋状态,尝试等待锁的释放。

00

重量级锁

重量级锁是一种基于阻塞的锁,用于解决多个线程同时访问同一个资源时的互斥问题。当一个线程尝试获取重量级锁时,如果该锁已经被其他线程占用,那么当前线程会进入阻塞状态,直到该锁被释放并且当前线程重新获得锁。重量级锁的实现基于操作系统的线程调度器,线程的阻塞和唤醒需要操作系统进行调度,开销较大。

10

需要注意的是,以上锁的类型是在 Java 虚拟机层面实现的,而不是 Java 语言层面的。不同的 Java 虚拟机实现可能会有所不同。

2.描述下锁升级?

这几个状态会随着竞争情况逐渐升级.锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁.这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

【并发编程进阶3】锁升级_无锁_02

3.偏向锁的原理?

为了在只有一个线程执行同步块时提高性能,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1 (表示当前是偏向锁):

  • 如果没有设置,则使用 CAS 竞争锁;
  • 如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

当设置了 HashCode 后,不能设置偏向锁,只能升级为轻量级锁,因为 HashCode 生成后,没办法把 HashCode 占位改为偏向线程的 id,这里 HashCode 会存在显式调用(直接调用 HashCode 方法)和隐式调用(比如 HashMap 的 put 方法)

4.JVM 偏向锁可以关闭吗?

偏向锁在 Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动 4 秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟:

-XX:BiasedLockingStartupDelay=0

如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM 参数关闭偏向锁:

-XX:-UseBiasedLocking=false

那么程序默认会进入轻量级锁状态。

锁相关的参数

-XX:+UseBiasedLocking 启用偏向锁,默认启用
-XX:+PrintFlagsFinal 打印JVM所有参数
-XX:BiasedLockingStartupDelay=4000 偏向锁启用延迟时间,默认4秒
-XX:BiasedLockingBulkRebiasThreshold=20 批量重偏向阈值,默认20
-XX:BiasedLockingBulkRevokeThreshold=40 批量撤销阈值,默认40
-XX:BiasedLockingDecayTime=25000

5.偏向锁撤销原理?

偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”,偏向标志位为 0)或轻量级锁(标志位为“00”)的状态。

如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

【并发编程进阶3】锁升级_sed_03

6.批量重偏向和批量撤销?

rebias & revoke
bulk rebias(批量重偏向):如果已经偏向 t1 线程的对象,在 t2 线程申请锁时撤销偏向后升级为轻量级锁的对象数量达到一定值(20),后续的申请会批量重偏向到 t2 线程;
bulk revoke(批量撤销):在单位时间(25s)内某种 Class 的对象撤销偏向的次数达到一定值(40),JVM 认定该 Class 竞争激烈,撤销所有关联对象的偏向锁,且新实例也是不可偏向的;并且有第三条线程 C 加入了,这个时候会触发批量撤销。JVM 会标记该对象不能使用偏向锁,以后新创建的对象,直接以轻量级锁开始。这个时候,才是真正的完成了锁升级。

7.轻量级锁加锁和解锁的过程?

轻量级锁是为了在线程交替执行同步块时提高性能。

轻量级锁加锁:

  • 线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录空间中,官方称为 Displaced Mark Word;
  • 然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
  • 如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级解锁:会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生.如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

8.Mark Word 说明

Mark Word 64 位,格式如下:

【并发编程进阶3】锁升级_多线程_04

java 对象头长度:

  • 如果是无锁状态,则分布为 25 位 unused,31 位 hashcode,1 位 unused,4 位 age,偏向标志位,锁的标志位
  • 如果是偏向锁状态,54 位 thread 的 id,2 位 epoch,1 位 unused,4 位 age,偏向标志位,锁的标志位
  • 如果是轻量级锁,62 位的 lock_record,锁的标志位
  • 如果是重量级锁,62 位的 monitor 指针,锁的标志位,owner 指向持有锁的线程

9.Mark Word 中的 epoch

epoch 是 Mark Word 中的一部分,只存在于偏向锁状态,占 2 位。锁对应的类同样用 2 位存了 epoch。

epoch 的作用就是标记偏向的合法性,说通俗点,就是看这个偏向锁有没有其他线程正在用。如果不用 epoch,那每次想要重偏向,都得去遍历所有的线程栈看看有没有其他线程在用。

每次撤销数量刚到 20 的时候,锁的类 epoch 都会+1,并且更新加锁状态的同类锁对象,那么那些不加锁的锁对象 epoch 就和类的 epoch 不一样了,那就可以知道哪些偏向锁是空闲的了。

epoch 只有两位,肯定会循环,但不会影响准确性。

10.synchronized 锁信息在对象中存储?

synchronized 用的锁是存在 Java 对象头(Mark Word)里的。如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit.数组的 1 字宽(4 字节)存储数组的长度信息.

无锁状态下 32 位 JVM 的 Mark Word 的默认存储结构如下:

【并发编程进阶3】锁升级_sed_05

有锁状态的 Mark Word 的信息变化如下,并从下图中能够看到锁的信息的确是放到 Mark Word 中的,并且不同的锁类型, Mark Word 中的信息会有变化。

【并发编程进阶3】锁升级_Word_06

11.偏向锁和轻量级锁区别?

偏向锁和轻量级锁都是为了提高多线程并发访问的性能,但是它们的实现方式和适用场景有所不同。

  1. 实现方式:偏向锁和轻量级锁的实现方式不同。偏向锁在对象头中记录拥有锁的线程 ID,判断锁状态时只需要比较锁的标记位和当前线程的 ID 是否相等即可。轻量级锁则需要使用 CAS 操作来进行锁操作,同时需要使用锁记录来存储锁的状态和拥有者线程的 ID。
  2. 适用场景:偏向锁适用于线程之间的竞争较少的情况,例如对象被一个线程锁定后,其他线程很少访问该对象。在这种情况下,使用偏向锁可以避免无谓的锁竞争,从而提高性能。轻量级锁适用于线程之间的竞争较多,但是锁持有时间较短的情况。在这种情况下,使用轻量级锁可以减少线程的上下文切换和阻塞唤醒的开销,从而提高性能。
  3. 锁状态转换:偏向锁和轻量级锁的锁状态转换也有所不同。偏向锁在有其他线程竞争时会立即升级成轻量级锁或重量级锁,而轻量级锁在自旋超过一定次数或竞争线程过多时会膨胀成重量级锁。

12.描述下锁升级的过程?

偏向锁:

  1. A 线程获取偏向锁,并且 A 线程死亡退出。B 线程争抢偏向锁,会直接升级当前对象的锁为轻量级锁。这只是针对我们争抢了一次。
  2. A 线程获取偏向锁,并且 A 线程没有释放偏向锁,还在 sync 的代码块里边。B 线程此时过来争抢偏向锁,会直接升级为重量级锁。
  3. A 线程获取偏向锁,并且 A 线程释放了锁,但是 A 线程并没有死亡还在活跃状态。B 线程过来争抢,会直接升级为轻量级锁。综上所述,当我们尝试第一次竞争偏向锁时,如果 A 线程已经死亡,升级为轻量级锁;如果 A 线程未死亡,并且未释放锁,直接升级为重量级锁;如果 A 线程未死亡,并且已经释放了锁,直接升级为轻量级锁。
  4. A 线程获取偏向锁,并且 A 线程没有释放偏向锁,还在 sync 的代码块里边。B 线程多次争抢锁,会在加锁过程中采用重量级锁;但是,一旦锁被释放,当前对象还是会以轻量级锁的初始状态执行。
  5. A 线程获取偏向锁,并且 A 线程释放了锁,但是 A 线程并没有死亡还在活跃状态。B 线程过来争抢。部分争抢会升级为轻量级锁;部分争抢会依旧保持偏向锁。

偏向锁到轻量级锁:

线程 1 作为持有者,线程 2 作为竞争者出现了,线程 2 由于 cas 替换偏向锁中的线程 id 失败,发起了撤销偏向锁的动作.此时线程 1 还存活,暂停了线程 1 的线程,此时线程 1 的栈中的锁记录会被执行遍历,将对象头中的锁的是否是偏向锁位置改成 0,并将锁标志位从 01(偏向锁)改成 00(轻量级锁),升级为轻量级锁。

【并发编程进阶3】锁升级_Word_07

轻量级锁到重量级锁:

线程 1 为锁的持有者,线程 2 为竞争者.线程 2 尝试 CAS 操作将轻量级锁的指针指向自己栈中的锁记录失败后。发起了升级锁的动作。线程 2 会将 Mark Word 中的锁指针升级为重量级锁指针。自己处于阻塞状态,因为此时线程 1 还没释放锁。当线程 1 执行完同步体后,尝试 CAS 操作将 Displaced Mark Word 替换回到对象头时,此时肯定会失败,因为 mark word 中已经不是原来的轻量级指针了,而是线程 2 的重量级指针.那么此时线程 1 很无奈,只能释放锁,并唤醒其他线程进行锁竞争。此时线程 2 被唤醒了,获取了重量级锁。

【并发编程进阶3】锁升级_sed_08

13.比较三种锁的优缺点及使用场景?

【并发编程进阶3】锁升级_Word_09

其实偏向锁,本就为一个线程的同步访问的场景.在出现线程竞争非常小的环境下,适合偏向锁。轻量级锁自旋获取线程,如果同步块执行很快,能减少线程自旋时间,采用轻量级锁很适合。重量级锁就不用多说了,synchronized 就是经典的重量级锁。使用 synchronized 不一定会升级为重量级锁,得看条件.

偏向锁:自始至终,对这把锁都不存在竞争,只需要做个标记,这就是偏向锁,每个对象都是一个内置锁(内置锁是可重入锁),一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有线程来访问并尝试获取锁的时候,它就会把这个线程记录下来,以后如果获取锁的线程正式偏向锁的拥有者,就可以直接获得锁,偏向锁性能最好。

轻量级锁:轻量级锁是指原来是偏向锁的时候,这时被另外一个线程访问,存在锁竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

重量级锁:重量级锁是互斥锁,主要是利用操作系统的同步机制实现的,当多个线程直接有并发访问的时候,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就升级为重量级锁,重量级锁会使得其他拿不到锁的线程陷入阻塞状态,重量级锁的开销相对较大。

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

  • 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用 CAS 操作,并将对象头中的 ThreadID 改成自己的 ID,之后再次访问这个对象时,只需要对比 ID,不需要再使用 CAS 在进行操作。
  • 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了。
  • 检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程。
  • 如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,
  • 如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁;
  • 如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
  • 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止 CPU 空转。

14.为什么要引入轻量级锁?

解答这个问题,先要自问一句,不引入轻量级锁,直接用重量级锁有什么坏处。我们知道重量级锁,如果线程竞争锁失败,会直接进入阻塞(Blocked)状态,阻塞线程需要 CPU 从用户态转到内核态,代价较大,假设一个线程刚刚阻塞不久这个锁就被释放了,这个线程被唤醒后,还需要从内核态切换到用户态,一来一回就两次状态切换,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋的等待锁释放。如果自旋的等待锁的释放,正好是我们的轻量级锁的特性,那么为什么引入轻量级锁就明白了。

JVM 如何开启轻量级锁:

JDK 1.5 使用- XX:+UseSpinning 手动开启。

JDK1.6 及后续版本默认开启轻量级锁。

15.什么是适应性自旋?

和普通自旋的区别?

JDK 1.5 的自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。自旋次数可以设定,通过自行设置自旋次数,此处举例说明设置为 10 次。

-XX:PreBlockSpin=10

在 JDK 1.6 引入了适应性自旋锁,XX:PreBlockSpin 参数也就没有用了.适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如下三点优化非常突出。

  1. 如果平均负载小于 CPU 则一直自旋
  2. 如果有超过(CPU/2)个线程正在自旋,则后来线程直接阻塞(升级为重量级锁)
  3. 如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞(升级为重量级锁)

觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

【并发编程进阶3】锁升级_多线程_10