java 线程——偏向锁&轻量级锁&重量级锁

         线程阻塞的代价

Mark word 

偏向锁

轻量级锁

轻量级锁的加锁过程

释放线程锁的过程

重量级锁


线程阻塞的代价

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源。

我们所熟知的Synchronized 在竞争锁失败的情况下就会进入阻塞状态,这种线程策略效率非常的低。

Mark word 

我们先来看看虚拟机中对象的结构,我们主要来看一下对象头。对象头信息是是与对象自身信息无关的额外存储成本,考虑到虚拟机的空间效率,虚拟机被设计成了一个非固定长度的数据结构以便在极小的空间内尽量存储更多的信息,它会根据对象的状态复用自己的存储空间。

例如在32 位的HotSpot 虚拟机中对象未锁定的状态下,Mark Word 的32 bit 空间中的25bit被用于存储对象的哈希吗,4 bit 用于存储对象分代年龄,2bit用于存储对象的锁标志位,1bit固定为0,在其他状态下对象的存储内容见下图

偏向锁

偏向锁是在JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进步提高程序的性能。

假设当前虚拟机启用了偏向锁,当锁对象第一次被线程获取的时候,虚拟机会把对象头中的锁标志为设置为“01”,同时用CAS 操作把获取这把锁的线程ID 记录在对象头之中,如果CAS 成功,持有偏向锁的线程以后每次进入这个锁的相关代码块时,虚拟机都不用再进行任何同步操作。

当另一个线程尝试获取这个锁的时候,偏向模式结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或者轻量级锁状态。

上面这段解释来自于《深入理解 java 虚拟机》

从这段话我们可以看出偏向锁是在没有线程竞争的情况下使用,当有现成来竞争时,偏向锁便膨胀为轻量级锁。

轻量级锁

轻量级锁是从偏向锁膨胀来的。

轻量级锁的加锁过程

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:

这里写图片描述

拷贝对象头中的Mark Word复制到锁记录中; 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

  

如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

释放线程锁的过程

它的解锁过程也是通过CAS 操作来进行的 。

如果对象Mark Word 仍指着线程的锁记录,那就用CAS 把对象当前的Mark Word 和线程中复制的Displaced Mark Word 替换回来。

若替换成功,整个同步过程就完成了。

若替换失败,说明有其它线程尝试获取过该锁,那就在释放锁的同时唤醒被挂起的线程。

偏向锁、轻量级锁的状态转化以及对象Mark Word 的关系如图

重量级锁

重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。

猜你喜欢

转载自blog.csdn.net/Alyson_jm/article/details/82820813