Synchronized锁的升级过程

synchronized锁升级过程依次为无锁、偏向锁、轻量级锁、重量级锁,部分文章认为synchronized锁不存在降级过程,但在openjdk的hotsopt jdk8u里是有锁降级的机制的。

对象头

Synchronized锁的升级过程_第1张图片

锁升级示意图

Synchronized锁的升级过程_第2张图片

1. 无锁到偏向锁

线程A执行到同步代码块时,检查对象头锁标志位是否为01,再看偏向锁标志位是否为0(即检查对象是否为无锁状态),通过CAS操作尝试修改MarkWord字段,这里CAS操作只尝试一次,失败的话说明发生锁竞争,立即升级为轻量级锁。成功修改后执行同步代码块,执行完毕后不主动释放偏向锁。将Lock Record中的obj设置为null,表示当前线程不在同步代码块中。

2. 偏向锁到轻量级锁

线程B执行到同步代码块时,检查到对象头锁标志位为01且偏向锁标志位为1,说明该锁已被线程占有。检查Thread Id是否为当前线程(B线程),是的话就执行同步代码块,否则就进入锁撤销逻辑,jvm会在安全点暂停所有线程,判断持有锁线程的当前状态,分两种情况:

  • 线程已死亡或没在执行同步代码块: JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。进行锁撤销操作,对象由偏向锁变为无锁且不可偏向状态,线程B通过CAS操作尝试修改Thread Id失败升级为轻量级锁,成功则执行同步代码块。
  • 线程正在执行同步代码块: 说明发生了锁竞争,进行锁撤销,将对象头MarkWord置为无锁状态并升级为轻量级锁。

注: 当锁处于偏向锁状态时,只要有多个线程尝试获取同一把锁,该锁都会升级为轻量级锁,区别是升级的时机不同。

3. 轻量级锁到重量级锁

线程C执行到同步代码块时,检查到对象头锁标志位00,说明该锁为轻量级锁。通过CAS操作尝试修改MarkWord字段,因为自旋的原因这里尝试多次CAS操作,多次尝试都失败的话将锁升级为重量级锁,线程睡眠等待被唤醒。自旋成功则执行同步代码块,执行完毕后释放锁恢复MarkWord数据,然后检查锁是否升级为重量级锁(线程执行同步代码块的时候可能有其他线程过来抢占锁且不成功,这时锁会升级为重量级锁)。如果升级为重量级锁的话还要执行一步操作 唤醒其他线程

轻量级锁的获取与释放

获取: 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录(Lock Recored)的空间,并将对象头中的MarkWord复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,执行同步代码块。(当线程再次进入同步代码块时,只需检查对象头中的MarkWord中的指针是否指向当前线程的锁记录,不是的话执行自旋CAS操作,是的话直接执行同步代码块)如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。多次自旋仍未获取锁则锁升级为重量级锁。
释放: 线程获取锁且执行完同步代码块,开始释放锁,通过CAS操作将之前复制在栈桢中的 Displaced Mard Word 替换回 MarkWord 中。如果成功说明执行同步代码块和释放过程中没有其他线程竞争,如果失败说明锁已经升级为重量级锁,这时候唤醒其他进程。

相关问题

轻量级锁竞争中的自旋等待

jvm进行线程之间的切换需要耗费一定的时间,部分情况切换线程耗费的时间比同步代码块执行的时间还要长。这时线程在获取不到锁的情况下自旋一定次数之后极有可能碰到获得锁的线程释放锁,所以,轻量级锁适用于那些同步代码块执行很快的场景,这样线程原地等待很短的时间就能够获得锁了。自适应自旋: jdk1.7之后自旋次数由jvm自动调整,判断依据是前一次在同一个锁上自旋的时间以及锁的拥有者的状态

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

锁粗化

在使用锁的时候,需要让同步块的作用范围尽可能小,这样做的目的是为了使需要同步的操作数量尽可能小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

批量重偏向与批量撤销

当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到SafePoint时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。详细可以看这篇文章。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。
存在如下两种情况:

  1. 一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。
  2. 存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。
当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

参考文章:
jvm锁升级的过程
死磕Synchronized底层实现–偏向锁
批量重偏向和批量撤销理解

技术博客地址: 西贝博客

你可能感兴趣的:(其他,java,并发编程,synchronized,锁)