美文网首页
Concurrency-锁升级

Concurrency-锁升级

作者: 甜甜起司猫_ | 来源:发表于2021-06-29 00:44 被阅读0次

    concurrency-锁升级

    4种锁状态:

    1. 无锁
    2. 偏向锁
    3. 轻量级锁
    4. 重量级锁

    无锁

    jvm会有4秒的偏向锁开启的延迟时间,在这个偏向延迟内对象处于为无锁态。如果关闭偏向锁启动延迟、或是经过4秒且没有线程竞争对象的锁,那么对象会进入无锁可偏向状态。

    准确来说,无锁可偏向状态应该叫做匿名偏向(Anonymously biased)状态,因为这时对象的mark word中后三位已经是101(无锁应该是001),但是threadId指针部分仍然全部为0,它还没有向任何线程偏向。综上所述,对象在刚被创建时,根据jvm的配置对象可能会处于无锁匿名偏向两个状态。(即使偏向锁标记位为1,但是threadId为0,所以是属于匿名偏向状态)

    如果在jvm的参数中关闭偏向锁-XX:-UseBiasedLocking,那么直到有线程获取这个锁对象之前,会一直处于无锁不可偏向状态001。

    在无锁状态下,为什么要存在一个不可偏向状态呢?为了减少锁升级步骤,降低性能损耗

    JVM内部的代码有很多地方也用到了synchronized,明确在这些地方存在线程的竞争,如果还需要从偏向状态再逐步升级,会带来额外的性能损耗,所以JVM设置了一个偏向锁的启动延迟,来降低性能损耗

    也就是说,在无锁不可偏向状态下,如果有线程试图获取锁,那么将跳过升级偏向锁的过程,直接使用轻量级锁。(跳级减少减少锁升级步骤,降低性能损耗)

    特殊情况
    就是匿名偏向状态下101,如果调用系统的hashCode()方法,会使对象回到无锁态001,并在markword中写入hashCode。并且在这个状态下,如果有线程尝试获取锁,会直接从无锁升级到轻量级锁,不会再升级为偏向锁。也就是说,无锁可偏向+hashcode状态下也会触发跳级

    偏向锁

    匿名偏向状态是偏向锁的初始状态,在这个状态下第一个试图获取该对象的锁的线程,会使用CAS操作(汇编命令CMPXCHG)尝试将自己的threadID写入对象头的mark word中,使匿名偏向状态升级为已偏向(Biased)的偏向锁状态。在已偏向状态下,线程指针threadID非空,且偏向锁的时间戳epoch为有效值。

    如果之后有线程再次尝试获取锁时,需要检查mark word中存储的threadID是否与自己相同即可,如果相同那么表示当前线程已经获得了对象的锁,不需要再使用CAS操作来进行加锁。

    如果mark word中存储的threadID与当前线程不同,那么将执行CAS操作,试图将当前线程的ID替换mark word中的threadID。只有当对象处于下面两种状态中时(可偏向),才可以执行成功:

    1. 对象处于匿名偏向状态
    2. 对象处于可重偏向(Rebiasable)状态,新线程可使用CAS将threadID指向自己

    如果对象不处于上面两个状态,说明锁存在线程竞争,在CAS替换失败后会执行偏向锁撤销操作。偏向锁的撤销需要等待全局安全点Safe Point(安全点是 jvm为了保证在垃圾回收的过程中引用关系不会发生变化设置的安全状态,在这个状态上会暂停所有线程工作),在这个安全点会挂起获得偏向锁的线程。

    在暂停线程后,会通过遍历当前jvm的所有线程的方式,检查持有偏向锁的线程状态是否存活:

    • 如果线程还存活,且线程正在执行同步代码块中的代码,则升级为轻量级锁
    • 如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向:
      1 不允许重偏向,则撤销偏向锁,将mark word升级为轻量级锁,进行CAS竞争锁
      2 允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向新线程

    综上小结

    1. 可偏向时CAS替换threadID,此时还是偏向锁状态。
    2. 不可偏向时(无锁不可偏向/无锁可偏向+hashcode/CAS替换失败)说明存在竞争,此时会升级为轻量级锁。

    偏向锁升级过程

    除了匿名偏向状态可以变为无锁态或升级为偏向锁,偏向锁还有其他状态的改变:

    • 偏向锁升级为轻量级锁,在执行完成同步代码后释放锁,变为无锁不可偏向状态。

    • 偏向锁升级为重量级锁,可以看到在调用了对象的wait()方法后,直接从偏向锁升级成了重量级锁,并在锁释放后变为无锁态

    这里是因为wait()方法调用过程中依赖于重量级锁中与对象关联的monitor,在调用wait()方法后monitor会把线程变为WAITING状态,所以才会强制升级为重量级锁。除此之外,调用hashCode方法时也会使偏向锁直接升级为重量级锁。

    轻量级锁

    原理

    1. 在代码访问同步资源时,如果锁对象处于无锁不可偏向状态,jvm首先将在当前线程的栈帧中创建一条锁记录(lock record),用于存放(这一步主要是copy mark word)

      • displaced mark word(置换标记字):存放锁对象当前的mark word的拷贝
      • owner指针:指向当前的锁对象的指针,在拷贝mark word阶段暂时不会处理它
    2. 在拷贝mark word完成后,首先会挂起线程,jvm使用CAS操作尝试将对象的 mark word中的 lock record 指针指向栈帧中的锁记录,并将锁记录中的owner指针指向锁对象的mark word。

      • 源mark word中的lock record指向栈桢中的lock record
      • 栈桢中的owner指向源mark word

      说白了就是新旧mark word互相指向

      进一步判断:

      • 如果CAS替换成功,表示竞争锁对象成功,则将锁标志位设置成 00,表示对象处于轻量级锁状态,执行同步代码中的操作
      • 如果CAS替换失败,则判断当前对象的mark word是否指向当前线程的栈帧:
        • 如果是则表示当前线程已经持有对象的锁,执行的是synchronized的锁重入过程,可以直接执行同步代码块
        • 否则说明该其他线程已经持有了该对象的锁,如果在自旋一定次数后仍未获得锁(多次自旋后还是无法获取锁会浪费CPU资源),那么轻量级锁需要升级为重量级锁,将锁标志位变成10,后面等待的线程将会进入阻塞状态
    3. 轻量级锁的释放同样使用了CAS操作,尝试将displaced mark word 替换回mark word,这时需要检查锁对象的mark word中lock record指针是否指向当前线程的锁记录:

      • 如果替换成功,则表示没有竞争发生,整个同步过程就完成了
      • 如果替换失败,则表示当前锁资源存在竞争,有可能其他线程在这段时间里尝试过获取锁失败,导致自身被挂起,并修改了锁对象的mark word升级为重量级锁,最后在执行重量级锁的解锁流程后唤醒被挂起的线程

    轻量级锁重入

    根据上面原理中的第2步,可以判断出,锁重入的条件是靠栈桢中lock record中的mark word和锁对象头中的mark word是否对应。

    轻量级锁的每次重入,都会在栈中生成一个lock record,但是保存的数据不同:

    1. 首次分配的lock record,displaced mark word复制了锁对象的mark word,owner指针指向锁对象
    2. 之后重入时在栈中分配的lock record中的displaced mark word为null,只存储了指向对象的owner指针

    轻量级锁中,重入的次数等于该锁对象在栈帧中lock record的数量,这个数量隐式地充当了锁重入机制的计数器。这里需要计数的原因是每次解锁都需要对应一次加锁,只有最后解锁次数等于加锁次数时,锁对象才会被真正释放。在释放锁的过程中,如果是重入则删除栈中的lock record,直到没有重入时则使用CAS替换锁对象的mark word。(原来获取锁就是替换对象的mark word)

    轻量级锁升级

    很明显升级条件就是线程不再交替获取锁,而是存在竞争,竞争的表现在于其中一条线程长期处于CAS自旋状态。

    在jdk1.6以前,默认轻量级锁自旋次数是10次,如果超过这个次数或自旋线程数超过CPU核数的一半,就会升级为重量级锁。这时因为如果自旋次数过多,或过多线程进入自旋,会导致消耗过多cpu资源,重量级锁情况下线程进入等待队列可以降低cpu资源的消耗。自旋次数的值也可以通过jvm参数-XX:PreBlockSpin进行修改。

    jdk1.6以后加入了自适应自旋锁 (Adapative Self Spinning),自旋的次数不再固定,由jvm自己控制,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

    1. 对于某个锁对象,如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而允许自旋等待持续相对更长时间
    2. 对于某个锁对象,如果自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    轻量级锁总结

    轻量级锁与偏向锁类似,都是jdk对于多线程的优化,不同的是轻量级锁是通过CAS来避免开销较大的互斥操作,而偏向锁是在无资源竞争的情况下完全消除同步。

    轻量级锁的“轻量”是相对于重量级锁而言的,它的性能会稍好一些。轻量级锁尝试利用CAS,在升级为重量级锁之前进行补救,目的是为了减少多线程进入互斥,当多个线程交替执行同步块时,jvm使用轻量级锁来保证同步,避免线程切换的开销,不会造成用户态与内核态的切换。但是如果过度自旋,会引起cpu资源的浪费,这种情况下轻量级锁消耗的资源可能反而会更多。

    重量级锁

    monitor 管程

    重量级锁是依赖对象内部的monitor(监视器/管程)来实现的 ,而monitor 又依赖于操作系统底层的Mutex Lock(互斥锁)实现,这也就是为什么说重量级锁比较“重”的原因了,操作系统在实现线程之间的切换时,需要从用户态切换到内核态,成本非常高

    当线程调用wait()方法,将释放当前持有的monitor,将owner置为null,进入WaitSet集合中等待被唤醒。当有线程调用notify()或notifyAll()方法时,也会释放持有的monitor,并唤醒WaitSet的线程重新参与monitor的竞争。

    wait和sleep的比较:wait释放锁,sleep不释放锁

    原理

    当升级为重量级锁的情况下,锁对象的mark word中的指针不再指向线程栈中的lock record,而是指向堆中与锁对象关联的monitor对象。当多个线程同时访问同步代码时,这些线程会先尝试获取当前锁对象对应的monitor的所有权:

    1. 获取成功,判断当前线程是不是重入,如果是重入那么recursions+1
    2. 获取失败,当前线程会被阻塞,等待其他线程解锁后被唤醒,再次竞争锁对象

    在重量级锁的情况下,加解锁的过程涉及到操作系统的Mutex Lock进行互斥操作,线程间的调度和线程的状态变更过程需要在用户态和核心态之间进行切换,会导致消耗大量的cpu资源,导致性能降低。

    重量级锁之所以重量,因为涉及切态,其他锁依赖cas原语避免了切态的消耗。

    相关文章

      网友评论

          本文标题:Concurrency-锁升级

          本文链接:https://www.haomeiwen.com/subject/ueuhultx.html