Java 多线程 - 锁优化(轻量级锁、偏向锁原理及锁的状态流转

2019-10-06  本文已影响0人  Richard_易

前言

记录在学习Java 多线程中 锁优化的有关知识点。

为了进一步改进高效并发,HotSpot虚拟机开发团队在JDK1.6版本上花费了大量精力实现各种锁优化。如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。(主要指的是synchronized的优化)。

适应性自旋 (自旋锁)

为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。引入自旋锁的原因是互斥同步对性能最大的影响是阻塞的实现,管钱线程和恢复线程的操作都需要转入内核态中完成,给并发带来很大压力。自旋锁让物理机器有一个以上的处理器的时候,能让两个或以上的线程同时并行执行。我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁

自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景

在 JDK 1.6之前,自旋次数默认是10次,用户可以使用参数-XX:PreBlockSpin来更改。

JDK1.6引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。(这个应该属于试探性的算法)。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行清除。锁清除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步枷锁自然就无需进行

简单来说,Java 中使用同步 来保证数据的安全性,但是对于一些明显不会产生竞争的情况下,Jvm会根据现实执行情况对代码进行锁消除以提高执行效率。

举例说明

对于一些看起来没有加锁的代码,其实隐式的加了很多锁,这些也是锁消除优化的对象。例如下面的字符串拼接代码就隐式加了锁:

image

String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为StringBuffer对象的连续 append()操作:

image

每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString()方法内部。也就是说,sb 的所有引用永远不会逃逸到concatString()方法之外,其他线程无法访问到它,因此可以进行消除。

image

锁粗化

轻量级锁 (@重点知识点)

JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)

重量级排序 :重量级锁 > 轻量级锁 > 偏向锁 > 无锁

先介绍一下HotSpot 虚拟机对象头的内存布局:

image

上面这些数据被称为Mark Word - 标记关键词。 其中 tag bits 对应了五个状态,这些状态的含义在右侧的 state 表格中给出。除了 marked for gc 状态(gc标记状态),其它四个状态已经在前面介绍过了。

下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息

image

简单来讲,轻量锁就是先通过CAS操作进行同步,因为绝大部分的锁,在整个同步周期都是不存在线程去竞争的。

获取轻量锁过程当中会当前线程的虚拟机栈中创建一个Lock Record的内存区域去存储获取锁的记录(类似于操作记录?),然后使用CAS操作将锁对象的Mark Word更新成指向刚刚创建的Lock Record的内存区域的指针,如果这个操作成功,就说明线程获取了该对象的锁,把对象的Mark Word 标记00,表示该对象处于轻量级锁状态。失败情况就如上所述,会判断是否是该线程之前已经获取到锁对象了,如果是就进入同步块执行。如果不是,那就是有多个线程竞争这个所对象,那轻量锁就不适用于这个情况了,要膨胀成重量级锁。

下图是对象处于轻量级锁的状态。

image

偏向锁 (@重点知识点)

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要

当锁对象第一次被线程获得的时候,进入偏向状态,标记为 |1|01|(前面内存布局图中说明了,这属于偏向锁状态)。同时使用 CAS 操作将线程 ID (ThreadID)记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。

引用《阿里手册:码出高效》的描述再理解一次:

可以结合下面这张锁的状态流转图理解一下:

image

上图实际上是摘自《深入理解Java虚拟机》,自己重新画了一次。在画图的过程当中,发现图中有两个点不是很理解,书中也没有对应的解释。就是偏向锁的重偏向撤销偏向时如果判断对象是否已经锁定

后面经过一段时间的查询才知道,HotSpot支持存储释放偏向锁,以及偏向锁的批量重偏向和撤销。这个特性可以通过JVM的参数进行切换,而且这是默认支持的。

Unlock状态下Mark Word的一个比特位用于标识该对象偏向锁是否被使用或者是否被禁止。如果该bit位为0,则该对象未被锁定,并且禁止偏向;如果该bit位为1,则意味着该对象处于以下三种状态:

这部分因为我目前暂时不想钻研这么深,就简单描述了一下状态流转机制,就当给自己留个坑先记录一下。想要更深的理解知识的话请需要参考下面的文章(使用关键词"bias revocation"进行搜索观看,第二篇写的很好,之后肯定要全篇好好拜读):

  1. Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing (这个是pdf版ppt文件,需要翻墙哦)

  2. Evaluating and improving biased locking in the HotSpot Virtual Machine(这篇我还查到了中文翻译,只不过只翻译了一点,也不保证翻译质量,看原文实际上更好点,讲的很透彻,详细讲了JVM偏向锁的机制,原理,批量重偏向、撤销偏向的操作,相关章节就在下方截图)

    image

StackOverflow上关于这个议题还有一个很有意思的问题,有兴趣的可以去看看。Does Java ever rebias an individual lock

通俗点总结

参考

  1. 《深入理解Java虚拟机》
  2. 《码出高效》
  3. The Hotspot Java Virtual Machine
  4. Biased Locking in HotSpot
  5. Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
  6. Evaluating and improving biased locking in the HotSpot Virtual Machine
  7. Does Java ever rebias an individual lock
上一篇下一篇

猜你喜欢

热点阅读