4

前置知识点:对象头

要了解锁优化策略中的轻量级锁与偏向锁的原理和运作过程,需要先了解Hotspot虚拟机的对象头部分的内存布局。

对象头(摘自《深入理解java虚拟机》)

对象头信息是与对象自身定义的数据无关的额外存储成本

如果对象是数组类型,则虚拟机用3个Word(字宽,在32位虚拟机中,一字宽等于四字节,即32bit)存储对象头。如果对象是非数组类型,则用2Word存储对象头。一个额外的字宽用于存储数组长度。

第一个字宽,用来存储对象自身的运行时数据 如:哈希吗(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,简称“Mark Word”。

第二个字宽用于存储指向方法区对象类型数据的指针。

对象头信息会根据对象的状态复用自己的存储空间。例如:在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32bit空间中的25bit用于存储对象哈希吗(HashCode),4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0。

整个对象头如下图:第三行即为第三部分,非数组类型的没有第三个字宽。

clipboard.png

Mark Word的默认存储结构如下图:

clipboard.png

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:

clipboard.png

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

clipboard.png

锁状态

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,

所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析。

clipboard.png

偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

配置

默认-XX:+UseBiasedLocking=true
-XX:-UseBiasedLocking=false关闭偏向锁
应用程序启动几秒钟之后才激活
-XX:BiasedLockingStartupDelay = 0关闭延迟

加锁流程

clipboard.png

获取偏向锁

根据上图,我们可以看到,首先判断锁对象是不是可偏向对象(若锁对象已经被轻量级锁定或者重量级锁定了,因为锁不会降级,所以它是不可偏向,同样的,关闭了偏向锁的设置-UseBiasedLocking=false,也会造成锁对象不可偏向

  1. 接下来,我们假定锁对象处于可偏向状态,并且ThreadID为0即biasable & unbiased状态(这里不讨论epoch和age)
  2. 当一个线程试图锁住一个处于biasable & unbiased状态的对象时,通过一个CAS将自己的ThreadID放置到Mark Word中相应的位置,如果CAS操作成功进入第(3)步否则进入(4)步
  3. 当进入到这一步时代表当前没有锁竞争,锁对象继续保持biasable可偏向状态,但是这时ThreadID字段被设置成了偏向锁所有者的ID,然后进入到第(6)步
  4. 当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程在持有偏向锁所有权。这个时候,就要判断持有偏向锁的线程是否还活着,因为一个线程执行完同步代码块后,不会主动释放偏向锁。如果持有偏向锁的线程还活着,将偏向锁消除,膨胀为轻量级锁,否则,将偏向锁消除,让争锁的线程持有偏向锁。
    具体过程是:当到达全局安全点(safepoint,在这个时间点上没有字节码正在执行)时拥有偏向锁的线程被挂起,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,锁对象有可能升级为轻量级锁状态(锁标志位置为00),阻塞在安全点的原持有线程被释放,进入到轻量级锁的执行路径中,继续往下执行同步代码。
  5. 当一个线程试图锁住一个处于biasable & biased并且ThreadID不等于自己的ID时,这时由于存在锁竞争必须进入到第(4)步来撤销偏向锁。
  6. 运行同步代码块

clipboard.png

轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

加锁过程

  1. 在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁记录目前的Mark Word的拷贝(称为Displaced Mark Word)以及记录锁对象的指针owner。

clipboard.png

  1. 当一个线程来获取这个锁,虚拟机将使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下:

clipboard.png

  1. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧。如果指向,说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。
    否则说明这个锁对象已经被其他线程抢占了。那么它就会自旋等待锁,一定次数后仍未获得锁对象,则修改mark word,将其修改为重量级锁的指针,表示该锁对象进入了重量锁状态。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。
  2. 由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间的,另一边,持有轻量级锁的线程,之前在获取锁的时候它拷贝了锁对象头的mark word,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对mark word做了修改,两者比对发现不一致导致释放锁的CAS失败,于是也切换到重量锁,释放轻量锁,并唤醒阻塞的线程。

clipboard.png


ls_cherish
77 声望9 粉丝

思否文章管理功能极差,以后不再更新,日后只维护[链接]