Java 15 废除偏差锁

JDK 15曾经在2020年9月15日公布,详情见 JDK 15 官网打算。其中有一项更新是废除偏差锁,官网的具体阐明在:JEP 374: Disable and Deprecate Biased Locking。

具体的阐明见:JDK 15已公布,你所要晓得的都在这里!

过后为什么要引入偏差锁?

偏差锁是 HotSpot 虚拟机应用的一项优化技术,可能缩小无竞争锁定时的开销。偏差锁的目标是假设 monitor 始终由某个特定线程持有,直到另一个线程尝试获取它,这样就能够防止获取 monitor 时执行 cas 的原子操作。monitor 首次锁定时偏差该线程,这样就能够防止同一对象的后续同步操作步骤须要原子指令。从历史上看,偏差锁使得 JVM 的性能失去了显著改善。

当初为什么又要废除偏差锁?

然而过来看到的性能晋升,在当初看来曾经不那么显著了。受害于偏差锁的应用程序,往往是应用了晚期 Java 汇合 API的程序(JDK 1.1),这些 API(Hasttable 和 Vector) 每次拜访时都进行同步。JDK 1.2 引入了针对单线程场景的非同步汇合(HashMap 和 ArrayList),JDK 1.5 针对多线程场景推出了性能更高的并发数据结构。这意味着如果代码更新为应用较新的类,因为不必要同步而受害于偏差锁的应用程序,可能会看到很大的性能进步。此外,围绕线程池队列和工作线程构建的应用程序,性能通常在禁用偏差锁的状况下变得更好。

偏差锁为同步零碎引入了许多简单的代码,并且对 HotSpot 的其余组件产生了影响。这种复杂性曾经成为了解代码的阻碍,也妨碍了对同步零碎进行重构。因而,咱们心愿禁用、废除并最终删除偏差锁。

思考

当初很多面试题都是讲述 CMS、G1 这些垃圾回收的原理,然而实际上官网在 Java 11 就曾经推出了 ZGC,号称 GC 方向的将来。对于锁的原理,其实 Java 8 的常识也须要更新了,毕竟技术始终在迭代,还是要不断更新本人的常识……学无止境……

话说回来偏差锁产生的起因,很大水平上是 Java 始终在兼容以前的程序,即便到了 Java 15,以前的 Hasttable 和 Vector 这种老古董性能差的类库也不会删除。这样做的益处很显著,然而害处也很显著,Java 要始终兼容这些代码,甚至影响 JVM 的实现。

本篇文章零碎整顿下 Java 的锁机制以及演进过程。

锁的倒退过程

在 JDK 1.5 之前,Java 是依附 Synchronized 关键字实现锁性能来做到这点的。Synchronized 是 JVM 实现的一种内置锁,锁的获取和开释是由 JVM 隐式实现。

到了 JDK 1.5 版本,并发包中新增了 Lock 接口来实现锁性能,它提供了与Synchronized 关键字相似的同步性能,只是在应用时须要显示获取和开释锁。

Lock 同步锁是基于 Java 实现的,而 Synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和开释锁操作都会带来用户态和内核态的切换,从而减少零碎性能开销。因而,在锁竞争强烈的状况下,Synchronized同步锁在性能上就体现得十分蹩脚,它也常被大家称为重量级锁。

特地是在单个线程反复申请锁的状况下,JDK1.5 版本的 Synchronized 锁性能要比 Lock 的性能差很多。

到了 JDK 1.6 版本之后,Java 对 Synchronized 同步锁做了充沛的优化,甚至在某些场景下,它的性能曾经超过了 Lock 同步锁。

Synchronized

阐明:局部参考自 https://juejin.cn/post/684490...

Synchronized 的根底应用就不列举了,它能够润饰办法,也能够润饰代码块。

润饰办法

public synchronized void syncMethod() {    System.out.println("syncMethod");}

反编译的后果如下图所示,能够看到 syncMethod 办法的 flag 蕴含 ACC_SYNCHRONIZED 标记位。

润饰代码块

public void syncCode() {    synchronized (SynchronizedTest.class) {        System.out.println("syncCode");    }}

反编译的后果如下图所示,能够看到 syncCode 办法中蕴含 monitorentermonitorexit 两个 JVM 指令。

JVM 同步指令剖析

monitorenter

间接看官网的定义:

次要的意思是说:

每个对象都与一个 monitor 相关联。当且仅当 monitor 对象有一个所有者时才会被锁定。执行 monitorenter 的线程试图取得与 objectref 关联的 monitor 的所有权,如下所示:

  • 若与 objectref 相关联的 monitor 计数为 0,线程进入 monitor 并设置 monitor 计数为 1,这个线程成为这个 monitor 的拥有者。
  • 如果该线程曾经领有与 objectref 关联的 monitor,则该线程从新进入 monitor,并减少 monitor 的计数。
  • 如果另一个线程曾经领有与 objectref 关联的 monitor,则该线程将阻塞,直到 monitor 的计数为零,该线程才会再次尝试取得 monitor 的所有权。

monitorexit

间接看官网的定义:

次要的意思是说:

  • 执行 monitorexit 的线程必须是与 objectref 援用的实例相关联的 monitor 的所有者。
  • 线程将与 objectref 关联的 monitor 计数减一。如果计数为 0,则线程退出并开释这个 monitor。其余因为该 monitor 阻塞的线程能够尝试获取该 monitor。

ACC_SYNCHRONIZED

官网的定义

JVM 对于办法级别的同步是隐式的,是办法调用和返回值的一部分。同步办法在运行时常量池的 method_info 构造中由 ACC_SYNCHRONIZED 标记来辨别,它由办法调用指令来查看。当调用设置了 ACC_SYNCHRONIZED 标记位的办法时,调用线程会获取 monitor,调用办法自身,再退出 monitor。

操作系统的管程(Monitor)

管程是一种在信号量机制上进行改良的并发编程模型

管程模型

管程的组成如下:

  • 共享变量
  • 入口期待队列
  • 一个锁:管制整个管程代码的互斥拜访
  • 0 个或多个条件变量:每个条件变量都蕴含一个本人的期待队列,以及相应的出/入队操作

ObjectMonitor

JVM 中的同步就是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 能够和对象一起创立、销毁。Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现,如下所示:

ObjectMonitor() {   _header = NULL;   _count = 0; //记录个数   _waiters = 0,   _recursions = 0;   _object = NULL;   _owner = NULL;   _WaitSet = NULL; //处于wait状态的线程,会被退出到_WaitSet   _WaitSetLock = 0 ;   _Responsible = NULL ;   _succ = NULL ;   _cxq = NULL ;   FreeNext = NULL ;   _EntryList = NULL ; //处于期待锁block状态的线程,会被退出到该列表   _SpinFreq = 0 ;   _SpinClock = 0 ;   OwnerIsThread = 0 ;}

本文应用的是 Java 11,其中有 sun.jvm.hotspot.runtime.ObjectMonitor 类,这个类有如下的初始化办法:

private static synchronized void initialize(TypeDataBase db) throws WrongTypeException {    heap = VM.getVM().getObjectHeap();    Type type  = db.lookupType("ObjectMonitor");    sun.jvm.hotspot.types.Field f = type.getField("_header");    headerFieldOffset = f.getOffset();    f = type.getField("_object");    objectFieldOffset = f.getOffset();    f = type.getField("_owner");    ownerFieldOffset = f.getOffset();    f = type.getField("FreeNext");    FreeNextFieldOffset = f.getOffset();    countField  = type.getJIntField("_count");    waitersField = type.getJIntField("_waiters");    recursionsField = type.getCIntegerField("_recursions");}

能够和 C++ 的 ObjectMonitor.hpp 的构造对应上,如果查看 initialize 办法的调用链,可能发现很多 JVM 的外部原理,本篇文章限于篇幅和内容起因,不去具体叙述了。

工作原理

Java Monitor 的工作原理如图:

当多个线程同时拜访一段同步代码时,多个线程会先被寄存在 EntryList 汇合中,处于 block 状态的线程,都会被退出到该 列表。接下来当线程获取到对象的 Monitor时,Monitor 是依附底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 胜利,则持有该 Mutex,其它线程将无奈获取到该 Mutex。

如果线程调用 wait() 办法,就会开释以后持有的 Mutex,并且该线程会进入 WaitSet 汇合中,期待下一次被唤醒。如果以后线程顺利执行完办法,也将开释 Mutex。

Monitor 依赖于底层操作系统的实现,存在用户态内核态的转换,所以减少了性能开销。然而程序中应用了 Synchronized 关键字,程序也不全会应用 Monitor,因为 JVM 对 Synchronized 的实现也有 3 种:偏差锁、轻量级锁、重量级锁。

锁降级

为了晋升性能,JDK 1.6 引入了偏差锁(就是这个曾经被 JDK 15 废除了)、轻量级锁重量级锁概念,来缩小锁竞争带来的上下文切换,而正是新增的 Java 对象头实现了锁降级性能。

Java 对象头

那么 Java 对象头又是什么?在 JDK 1.6 中,对象实例分为:

  • 对象头

    • Mark Word
    • 指向类的指针
    • 数组长度
  • 实例数据
  • 对齐填充

其中 Mark Word 记录了对象和锁无关的信息,在 64 位 JVM 中的长度是 64 位,具体信息如下图所示:

偏差锁

为什么要有偏差锁呢?偏差锁次要用来优化同一线程屡次申请同一个锁的竞争。可能大部分工夫一个锁都是被一个线程持有和竞争。如果一个锁被线程 A 持有,后开释;接下来又被线程 A 持有、开释……如果应用 monitor,则每次都会产生用户态和内核态的切换,性能低下。

作用:当一个线程再次拜访这个同步代码或办法时,该线程只需去对象头的 Mark Word 判断是否有偏差锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标记位还是 01,“是否偏差锁”标记位设置为 1,并且记录抢到锁的线程 ID,示意进入偏差锁状态。

一旦呈现其它线程竞争锁资源,偏差锁就会被撤销。撤销机会是在全局平安点,暂停持有该锁的线程,同时保持该线程是否还在执行该办法。是则降级锁;不是则被其它线程抢占。

高并发场景下,大量线程同时竞争同一个锁资源,偏差锁会被撤销,产生 stop the world后,开启偏差锁会带来更大的性能开销(这就是 Java 15 勾销和禁用偏差锁的起因),能够通过增加 JVM 参数敞开偏差锁:

-XX:-UseBiasedLocking //敞开偏差锁(默认关上)

-XX:+UseHeavyMonitors  //设置重量级锁

轻量级锁

如果另一线程竞争锁,因为这个锁曾经是偏差锁,则判断对象头的 Mark Word 的线程 ID 不是本人的线程 ID,就会进行 CAS 操作获取锁:

  • 胜利,间接替换 Mark Word 中的线程 ID 为以后线程 ID,该锁会放弃偏差锁。
  • 失败,标识锁有竞争,偏差锁会降级为轻量级锁。

轻量级锁的适用范围:线程交替执行同步块,大部分锁在整个同步周期外部存在场馆工夫的竞争

自旋锁与重量级锁

轻量级锁的 CAS 抢锁失败,线程会挂起阻塞。若正在持有锁的线程在很短的工夫内开释锁,那么刚刚进入阻塞状态的线程又要从新申请锁资源。

如果线程持有锁的工夫不长,则未获取到锁的线程能够一直尝试获取锁,防止线程被挂起阻塞。JDK 1.7 开始,自旋锁默认开启,自旋次数又 JVM 配置决定。

自旋锁重试之后如果抢锁仍然失败,同步锁就会降级至重量级锁,锁标记位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

在高负载、高并发的场景下,能够通过设置 JVM 参数来敞开自旋锁,优化性能:

-XX:-UseSpinning //参数敞开自旋锁优化(默认关上) -XX:PreBlockSpin //参数批改默认的自旋次数。JDK1.7后,去掉此参数,由jvm管制

再深入分析

锁到底锁的是什么呢?又是谁锁的呢?

当多个线程都要执行某个同步办法时,只有一个线程能够获取到锁,而后其余线程都在阻塞期待。所谓的“锁”动作,就是让其余的线程阻塞期待;那 Monitor 是何时生成的呢?我集体感觉应该是在多个线程同时申请的时候,生成重量级锁,一个对象才会跟一个 Monitor 相关联。

那其余的被阻塞的线程是在哪里记录的呢?就是在这个 Monitor 对象中,而这个 Monitor 对象就在对象头中。(如果不对,欢送大家留言探讨~)

锁优化

Synchronized 只在 JDK 1.6 以前性能才很差,因为这之前的 JVM 实现都是重量级锁,间接调用 ObjectMonitor 的 enter 和 exit。从 JDK 1.6 开始,HotSpot 虚拟机就减少了上述所说的几种优化:

  • 偏差锁
  • 轻量级锁
  • 自旋锁

其余还有:

  • 适应性自旋
  • 锁打消
  • 锁粗化

锁打消

这属于编译器对锁的优化,JIT 编译器在动静编译同步块时,会应用逃逸剖析技术,判断同步块的锁对象是否只能被一个对象拜访,没有公布到其它线程。

如果确认没有“逃逸”,JIT 编译器就不会生成 Synchronized 对应的锁申请和开释的机器码,就打消了锁的应用。

锁粗化

JIT 编译器动静编译时,如果发现几个相邻的同步块应用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而防止一个线程“重复申请、开释同一个锁“所带来的性能开销。

减小锁粒度

咱们在代码实现时,尽量减少锁粒度,也可能优化锁竞争。

总结

  • 其实当初 Synchronized 的性能并不差,偏差锁、轻量级锁并不会从用户态到内核态的切换;只有在竞争十分激烈的时候,才会降级到重量级锁。
  • Synchronized 的锁是由 JVM 实现的。
  • 偏差锁曾经被废除了。

参考

  1. https://juejin.cn/post/684490...
  2. 极客工夫:多线程之锁优化(上):深刻理解Synchronized同步锁的优化办法

公众号

coding 笔记、点滴记录,当前的文章也会同步到公众号(Coding Insight)中,心愿大家关注^_^

代码和思维导图在 GitHub 我的项目中,欢送大家 star!