带你整理面试过程中关于锁升级的过程

Posted 南淮北安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你整理面试过程中关于锁升级的过程相关的知识,希望对你有一定的参考价值。

文章目录

一、对象头

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

实例数据和对其填充与synchronized无关,实例数据存放类的属性数据信息,java代码中能看到的属性和他们的值。

对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。

对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关

对象头主要结构是由Mark Word 和Class Metadata Address(类型指针)组成,如果是数组的话,对象头里还包含一个数组的长度

Mark Word
其中 Mark Word 主要用于存储对象自身运行时的数据,如哈希吗、锁信息或分代年龄等信息

它被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,会根据对象的状态复用自己的存储空间

例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向下对象的存储内容。



在32位的JVM 中它的长度是32位,在64位的JVM中它的长度是64位

类型指针

Class Metadata Address是类型指针,JVM通过该指针确定该对象是哪个类的实例

二、重量级锁

重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大

synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此synchronized属于重量级锁。

重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高

三、轻量级锁

JDK在1.6版本以后,为了减少获取锁和释放锁所带来的性能消耗及提高性能,引入了轻量级锁和偏向锁。

轻量级锁是相对于重量级锁而言的:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁

如果偏向锁失败,那么虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。

轻量级锁的操作也很方便,它只是简单地将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。

如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。

轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁

四、偏向锁

锁偏向是一种针对加锁操作的优化手段。它的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。因此,对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次极有可能是同一个线程请求相同的锁。而对于锁竞争比较激烈的场合,其效果不佳。因为在竞争激烈的场合,最有可能的情况是每次都是不同的线程来请求相同的锁。这样偏向模式会失效,因此还不如不启用偏向锁。

除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。

偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS(Compare and Swap)原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。

在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时。

综上所述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能

五、自旋锁

自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。

线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。

在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。

(1)自旋锁的优缺点

优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。

缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。

(2)自旋锁的时间阈值
自旋锁用于让当前线程占着CPU的资源不释放,等到下次自旋获取锁资源后立即执行相关操作。

但是如何选择自旋的执行时间呢?如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源浪费。因此,对自旋的周期选择将直接影响到系统的性能!

JDK的不同版本所采用的自旋周期不同,JDK 1.5为固定DE时间,JDK1.6引入了适应性自旋锁。

适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间是就一个最佳时间。

六、锁的升级

锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级

比如在32位的JVM中:

(1)当没有被当成锁时,这就是一个普通的对象,Mark Word 记录对象的 HashCode,锁标志位是01,是否偏向锁那一位是0。

(2)当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,同时使用CAS操作把获取到这个锁的线程的id记录到对象的 Mark Word中,表示进入偏向锁状态。

这就是偏向锁比轻量级锁高效的原因,只需要在切换ThreadID时执行一次CAS原子操作

(3)当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

(4)当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。

(5)偏向锁抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为重量级锁。虚拟机首先在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方在这份拷贝加了一个Displace前缀,即 Displace Mark Word)

此时线程堆栈与对象头的状态如图:

然后虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向锁记录的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位将转为 “00”,表示此对象处于轻量级锁定状态。

此时线程堆栈和对象头的状态:

(6)如果更新操作失败,则首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果指向,说明当前线程已经拥有了这个对象的锁,就可以直接进入同步块继续执行。

(7)否则会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试

(8)自旋重试之后如果抢锁依然失败,说明这个锁对象已经被其他线程抢占了,如果有两条以上的线程争用同一个锁,那轻量级锁就不在生效,会膨胀为重量级锁,锁标志的状态值变为“10”

Mark Word 中存储的是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。

以上是关于带你整理面试过程中关于锁升级的过程的主要内容,如果未能解决你的问题,请参考以下文章

带你整理面试过程中关于锁的相关知识点上(synchronizedReentrantLock)

带你整理面试过程中关于 Mybatis 底层的相关知识

带你整理面试过程中关于多线程的编程问题

带你整理面试过程中关于ThreadLocal的相关知识

带你整理面试过程中关于Java 中多线程的创建方式的最全整理

带你整理面试过程中关于 Java 中的 异常分类及处理的相关知识