Java中的锁系列2

作者: 窝牛狂奔 | 来源:发表于2017-03-31 15:40 被阅读220次

    经常听到人家说什么偏向锁、轻量级锁、重量级锁、自旋锁,自适应自旋锁。每当这个时候,以前的我是一脸懵逼…

    那到底什么是偏向锁、轻量级锁、自旋锁和重量级锁?

    首先我们需要明确一个概念,所有的这些锁都是针对JVM的内容对象锁。是synchronized实现里相关的东西。所以,要了解这些锁,我们必须要知道synchronized在JVM里的实现原理。

    JVM基于进入和退出Monitor对象来实现synchronized。在synchronized代码块开始位置织入monitorenter,在结束的位置(正常结束和异常结束处)织入monitorexit指令。线程执行到monitorenter处,会获取锁对象锁对应的monitor的所有权,即尝试获得对象的锁。(任意对象都有一个monitor与之关联,当且一个monitor被持有后,他处于锁定状态)。

    一、对象里存了什么东西

    既然是对象锁,那我肯定要知道对象是啥。

    那java的对象结构到底是什么样子的呢?我们看下Hotspot JVM中,32位机器下,对象的存储内容。

    这个图一定要仔细看下。我们有时候也把第一个32位也叫MarkWord。

    比如一个Integer对象,实际大小是:4(MarkWord)+4(对象地址)+4(实际int值)+4(补全的对齐)。所以,下次人家问一个Integer占多少字节的时候,勇敢的说出来:16个字节,int的4倍。

    二、锁的状态和升级

    从上面的图中的MarkWord,我们可以知道一个对象有4种锁的状态,由低到高依次为:无锁状态。偏向锁、轻量级锁、重量级锁,其实这些都是锁的状态。并且JVM还规定锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

    三、偏向锁

    首先为什么要有偏向锁?

    偏向锁是为了在无锁竞争的情况下,避免在轻量级锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。

    那偏向锁是怎么做到减少CAS的原子指令的呢?

    我们来看下偏向锁的获取和释放流程:

    1、线程A获得锁,会在线程A的的栈帧里创建lock record(锁记录变量),则在锁对象的对象头里和lock record里存储线程A的线程id.以后该线程的进入,就不需要CAS操作,只需要判断是否是当前线程。

    2、线程A获取锁后,不会主动释放锁。直到线程B也要竞争该锁时,线程A才会释放锁。

    偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。

    JVM怎么开启/关闭偏向锁?

    偏向锁在jdk1.6之后是默认开启的。

    通过jvm的参数-XX:-UseBiasedLocking,可以关闭偏向锁,然后默认会进入轻量级锁。

    通过jvm的参数-XX:+UseBiasedLocking

    -XX:BiasedLockingStartupDelay=0,可以开启偏向锁,并且设置偏向锁的启动延迟为0(这个值默认为5秒,因为jvm认为系统刚刚启动的时候资源竞争是很激烈的。我们把这个值改为0,方便测试。)

    我们可以来看一下,开启和关闭偏向锁之后,系统的差距

    我们分别用两个参数来运行这段代码:

    1、关闭偏向锁。-XX:-UseBiasedLocking

    运行结果如下:

    2、开启偏向锁。-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0

    通过这个结果,我们可以大概的得出结论:在本例子中,完全无锁的情况下,开启偏向锁比关闭偏向锁,性能提升了5%到10%。

    四、轻量级锁

    轻量级锁(Lightweight Locking)本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用CAS尝试在进入互斥前,进行补救,避免调用操作系统层面的重量级互斥锁。

    线程A获得锁,会在线程A的栈帧里创建lock

    record(锁记录变量),让lock record的指针指向锁对象的对象头中的mark word.再让mark word指向lock record.这就是获取了锁。

    轻量级锁,线程B在锁竞争时,发现锁已经被线程A占用,则线程B不进入内核态,让线程B自旋(自旋的事情我们后面再讲),执行空循环,等待线程A释放锁。如果,完成自旋策略还是发现线程A没有释放锁,或者让线程C占用了。则线程B试图将轻量级锁升级为重量级锁。

    五、重量级锁

    重量级锁,就是让争抢锁的线程从用户态转换成内核态,让CPU借助操作系统进行线程协调。

    六、自旋锁

    前面在讲轻量级锁的时候,我们已经提到了自旋。本质就是不让出CPU,执行空循环,然后等待拿锁。

    使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义。但是,对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,如果依然无法获得对应的锁,反而浪费了系统的资源。

    我们来看下自己写的一个自旋锁。

    在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数,默认是10次。

    在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整,也就是开始使用自适应自旋锁。

    七、自适应自旋锁

    自适应意味着自旋的次数或者时间不再是固定的,而是由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚好成功获得锁, 并且在持有锁的线程在运行中,那么虚拟机就会认为这次自旋也是很有可能获得锁, 进而它将允许自旋等待相对更长的时间。

    总结:

    这些东西可能在写代码的时候完全用不到,正常情况下,只需要写个synchronized就完事了。但是,在虚拟机调优方面还是很有用的。当然,面试的时候也很有用。

    参考资料:

    http://www.cnblogs.com/shangxiaofei/p/5569879.html

    http://www.cnblogs.com/javaminer/p/3889023.html

    http://www.cnblogs.com/think-in-java/p/5520462.html

    相关文章

      网友评论

        本文标题:Java中的锁系列2

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