共计 7805 个字符,预计需要花费 20 分钟才能阅读完成。
前言
在很久之前,我在面实习生的时候,就有人问过我 synchronized 的锁降级过程,我过后只是浅浅理解,前面其实理解了锁降级的流程。但其实我并不是很明确,到底优化了哪里,到底是针对哪种场景进行优化,我其实更想得到这个锁降级过程中的引入场景。尤其是看到 JDK 15 废除并禁用了偏差锁之后,我其实在想为什么要移除这项技术,是 JDK 有了更好的优化,还是这项技术不再实用于当初。这里间接说答案吧,答案就在 JEP 374 中。我原本想间接贴答案的,然而思考到有的同学还不分明 synchronized 的降级流程,这里还是先简略的讲一下锁降级的流程。
其实这也是一道面试常见的问题,然而经常是面试官问我锁降级的过程,而不会问哪些场景会从锁降级中受害,这也是我经常纳闷的中央,不去问 why,而是问 what。
总有些口口流传的优化,大家都违心置信,然而咱们都要置信那句话: 没有考察就没有发言权。
synchronized 锁简介
这里咱们简略的温习一下 synchronized, synchronized 是咱们遇到的第一个同步工具,它有许多别名: 外部锁、排他锁、乐观锁。它可能保障原子性、可见性和有序性。synchronized 关键字润饰的办法就被称为同步办法 (Synchronized Method), synchronized 润饰的动态实例办法就被称为同步实例办法。同步办法的整个办法称为临界区。
// 润饰办法
public synchronized void synchronizedDemo(){}
// 润饰静态方法
public static synchronized void synchronizedStaticDemo(){}
public void synchronizedDemoPlus(){
// 润饰代码块
synchronized (this){}}
Java 平台的任何一个对象都有惟一一个与之关联的锁。线程进入临界区须要申请锁,那么锁放在哪里呢?答案是对象头,一个一般的 Java 对象的外部结构如下图所示:
一般来说,咱们对这个锁的认知是,多个线程进入临界区的时候,会申请取得这个锁,如果锁曾经被其余线程所获取,那么这个线程会陷入阻塞状态。
更为精确的形容是 JVM 会为每个外部锁调配一个入口集,用于记录期待取得相应外部锁的线程,当这些线程申请的锁被其持有线程开释的时候,该锁入口集中的一个任意线程会被 JVM 唤醒。看到这里可能有同学会问,下面提到了取得锁和开释锁,JVM 是怎么解决的呢。这个其实要借助反编译指令,这里咱们以同步代码块来察看 synchronized 的外部实现:
/**
* 这个代码要编译一下, 造成字节码,
* 其实问题的答案就在字节码上。*/
public class ThreadDemo{public void synchronizedDemo(){synchronized (this){}}
}
而后找到这个类对应的字节码所在的文件夹,关上命令行执行如下指令
javap -c ./ThreadDemo.class
monitorenter 代表进入临界区和申请锁指令,monitorexit 代表出临界区,开释锁指令。那为什么会有两个开释锁指令,这个问题问的好,最上面的那个开释锁指令是为临界区的代码出了异样筹备的。JVM 在实现 monitorenter 和 monitorexit 的时候须要借助一个原子操作 (CAS), 这个代价比拟低廉。
锁降级概述
然而咱们晓得 Java 中的线程是被映射到操作系统层面的线程的,所以唤醒还须要申请操作系统,如果一个线程持有锁的工夫不长,让线程陷入沉睡,再由操作系统去唤醒的代价就有些高。由此就引出了锁降级:
刚开始对象头的锁状态是无锁,线程在进入临界区执行代码的时候,如果获取锁胜利,JVM 就会为每个对象保护一个偏好 (Bias), 即一个对象对应的外部锁第一次被一个线程取得,那么这个线程就会被记录为该对象的偏好线程 (Biased Thread). 这个线程后续无论是再次申请还是开释锁,这个偏好线程都无需借助原先低廉的原子操作,从而缩小了锁的申请与开释的开销。
这种优化基于这样一种观测:锁在大多数状况下没有争用,并且这些锁在整个生命周期至少只会被一个线程持有。其实网上少数博客也是基于这种状况来介绍为什么引入偏差锁的。其实看到这句话,我很不了解,我用 synchronization 就是为了解决多线程竞争资源所带来的问题,那下面这种观测是基于哪种场景的呢?要答复这个问题,我首先提出一个问题: Java 线程平安的汇合有哪些?个别的同学可能会答复:
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteArraySet
这些汇合都是实现比拟精美的并发汇合,于 JDK1.5 被引入,其实还有一些不为人所熟知的并发平安汇合:
- Hashtable
- Vector
这两个汇合是 Java 的原始汇合,于 JDK 1.0 被引入,个别不会有人抉择应用这两个汇合,起因在于,这两个在线程平安上的实现是简略又粗犷,给每个办法加上 synchronized。我看了一下 ArrayList 和 HashMap 的引入工夫,是 JDK 1.2,所以晚期的 Java 程序员基本没有抉择,只有 Hashtable、Vector 能够应用,即便是单线程的应用场景,也没有 ArrayList、HashMap 能够用。Java 是一个向前兼容的语言,即便当初 JDK 19 曾经快公布了,有些我的项目的 JDK 依然停留在 JDK5、6。所以 JDK 6 引入偏差锁就是优化晚期 JDK 代码的性能。这也是 JDK 15 移除偏差锁的起因之一,更为致命的起因是启用偏差锁,会导致性能降落。
为什么移除偏差锁
让咱们认真的看下 JEP 374 这个提案为什么要移除偏差锁:
Biased locking is an optimization technique used in the HotSpot Virtual Machine to reduce the overhead of uncontended locking. It aims to avoid executing a compare-and-swap atomic operation when acquiring a monitor by assuming that a monitor remains owned by a given thread until a different thread tries to acquire it. The initial lock of the monitor biases the monitor towards that thread, avoiding the need for atomic instructions in subsequent synchronized operations on the same object. When many threads perform many synchronized operations on objects used in a single-threaded fashion, biasing the locks has historically led to significant performance improvements over regular locking techniques.
The performance gains seen in the past are far less evident today. Many applications that benefited from biased locking are older, legacy applications that use the early Java collection APIs, which synchronize on every access (e.g.,
Hashtable
andVector
). Newer applications generally use the non-synchronized collections (e.g.,HashMap
andArrayList
), introduced in Java 1.2 for single-threaded scenarios, or the even more-performant concurrent data structures, introduced in Java 5, for multi-threaded scenarios. This means that applications that benefit from biased locking due to unnecessary synchronization will likely see a performance improvement if the code is updated to use these newer classes. Furthermore, applications built around a thread-pool queue and worker threads generally perform better with biased locking disabled. (SPECjbb2015 was designed that way, e.g., while SPECjvm98 and SPECjbb2005 were not). Biased locking comes with the cost of requiring an expensive revocation operation in case of contention. Applications that benefit from it are therefore only those that exhibit significant amounts of uncontended synchronized operations, like those mentioned above, so that the cost of executing cheap lock owner checks plus an occasional expensive revocation is still lower than the cost of executing the eluded compare-and-swap atomic instructions. Changes in the cost of atomic instructions since the introduction of biased locking into HotSpot also change the amount of uncontended operations needed for that relation to remain true. Another aspect worth noting is that applications won’t have noticeable performance improvements from biased locking even when the previous cost relation is true when the time spend on synchronized operations is still only a small fraction of the total application workload.Biased locking introduced a lot of complex code into the synchronization subsystem and is invasive to other HotSpot components as well. This complexity is a barrier to understanding various parts of the code and an impediment to making significant design changes within the synchronization subsystem. To that end we would like to disable, deprecate, and eventually remove support for biased locking.
这里间接放谷歌翻译吧:
偏差锁定是 HotSpot 虚拟机中应用的一种优化技术,用于缩小非竞争锁定的开销。它旨在防止在获取监视器时执行比拟和替换原子操作,办法是假如监视器依然由给定线程领有,直到不同的线程尝试获取它。监视器的初始锁定将监视器偏差该线程,从而防止在对同一对象的后续同步操作中须要原子指令。当许多线程对以单线程形式应用的对象执行许多同步操作时,与惯例锁定技术相比,偏差锁定在历史上会导致显着的性能改良。
过来看到的性能晋升在明天远不那么显著。受害于偏差锁定的许多应用程序是应用晚期 Java 汇合 API 的较旧的遗留应用程序,这些 API 在每次拜访时都会同步(例如 Hashtable 和 Vector)。较新的应用程序通常应用非同步汇合(例如,HashMap 和 ArrayList),在 Java 1.2 中针对单线程场景引入,或者在 Java 5 中针对多线程场景引入了性能更高的并发数据结构。这意味着如果更新代码以应用这些较新的类,因为不必要的同步而受害于偏差锁定的应用程序可能会看到性能改良。此外,围绕线程池队列和工作线程构建的应用程序通常在禁用偏差锁定的状况下性能更好。(例如,SPECjbb2015 就是这样设计的,而 SPECjvm98 和 SPECjbb2005 则不是)。偏差锁定随同着在争用状况下须要低廉的撤销操作的老本。因而,受害于它的应用程序只有那些体现出大量非竞争同步操作的应用程序,如下面提到的那些,因而,执行便宜的锁所有者查看加上偶然低廉的撤销的老本依然低于执行规避的比拟和替换原子指令的老本。自从将偏差锁定引入 HotSpot 以来,原子指令老本的变动也扭转了放弃该关系正确所需的非竞争操作的数量。另一个值得注意的方面是,应用程序不会从偏差锁定中取得显着的性能改良,即便之前的老本关系是正确的,而花在同步操作上的工夫依然只是应用程序总工作负载的一小部分。自从将偏差锁定引入 HotSpot 以来,原子指令老本的变动也扭转了放弃该关系正确所需的非竞争操作的数量。另一个值得注意的方面是应用程序不会从偏差锁定中取得显着的性能改良,即便之前的老本关系是正确的,而花在同步操作上的工夫依然只是应用程序总工作负载的一小部分。自从将偏差锁定引入 HotSpot 以来,原子指令老本的变动也扭转了放弃该关系正确所需的非竞争操作的数量。另一个值得注意的方面是应用程序不会从偏差锁定中取得显着的性能改良,即便之前的老本关系是正确的,而花在同步操作上的工夫依然只是应用程序总工作负载的一小部分。
偏差锁定在同步子系统中引入了许多简单的代码,并且还侵入了其余 HotSpot 组件。这种复杂性是了解代码各个局部的阻碍,也是在同步子系统内进行重大设计更改的阻碍。为此,咱们心愿禁用、弃用并最终移除对偏差锁定的反对。
总结一下,引入偏差锁的目标,次要为了优化 JDK 1.2 之前的 HashTable、Vector,这两个汇合就是咱们下面说的,对应的锁在整个汇合生命周期,有的时候只会被一个线程所获取。当初移除偏差锁是因为根本没人用这俩汇合,再加上撤销偏差锁也须要昂扬的老本,所以 JDK 15 决定移除此个性。
轻量级锁到重量级锁?
那偏差锁咱们这里不提,咱们接着聊锁降级的流程:
后面是由无锁降级到偏差锁,假如有其余线程拜访偏差锁申请取得锁,那么此时偏差锁降级到轻量级锁,这个轻量级锁的具体表现为获取锁失败的线程,并不会陷入阻塞状态,而是会自旋,即不停的循环的去获取锁,然而长时间的自选比拟耗费 CPU 的资源,所以达到肯定次数之后,就会达到重量级锁,如果锁处于重量级锁状态,获取锁失败的线程将会进入阻塞状态。
《Java 多线程编程 实战指南》中看到的一种形容:
存在锁争用的状况下,一个线程申请一个锁的时候如果这个锁恰好被其余线程持有,那么这个线程就须要期待该锁被其持有线程开释,实现这种期待的一种激进办法就是暂停线程,然而暂停线程会导致上下文切换,因而对于一个具体锁实例来说,这种实现策略比拟适宜于零碎中绝大多数线程对该锁的持有选工夫较长的场景,这样才可能对消上下文切换的开销。另一个实现办法就是采纳忙等,所谓忙等相当于如下代码所示的一个循环体为空的循环语句:
while(lockIsHeldByOtherThread){}.
可见,忙等是通过重复执行空操作直到所需的条件成立为止而实现期待的。这种策略的益处是不会导致上下文切换,毛病是比拟耗费处理器资源。
事实上,JVM 也不是非要在上述两种实现策略之中择其一,它能够综合应用上述两种策略。对于一个具体的锁实例,JVM 会依据其运行过程中收集到的信息来判断这个锁是属于被线程持有工夫“较长”的还是“较短”的。对于被线程是由工夫“较长”的锁,JVM 会选取暂停期待策略,对于线程持有工夫“较短的锁”,JVM 会选取忙等期待策略。JVM 也可能先采纳忙等期待策略,在忙等失败的状况下再采纳暂停期待策略,JVM 虚拟机的这种优化被称为适应性锁。
其实我读到这段的时候,我在想,JVM 还会先采取暂停期待策略,再调整为忙等期待策略?遍寻全网还是没找到这块的阐述,相干的阐述只是在自适应锁上,自适应锁于 JDK 1.6 被引入,适应性锁能够了解为适应性自选锁,自适应意味着自选的工夫次数不再固定,而是由前一次在同一个锁上的自选工夫及锁的拥有者的状态来决定。如果在同一个锁对象上,自选期待刚刚胜利取得锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次胜利,进而它将自旋期待继续绝对更长时间。如果对于某个锁,自旋很少胜利取得过,那么当前尝试获取这个锁很可能将省略掉自选过程,间接阻塞线程,避免浪费处理器资源。
其实到这里问题基本上曾经完结了,咱们曾经基本上答复了锁降级的流程,在 JDK 8 之后的锁降级流程,该当是无锁 到 偏差锁,而后自选,JVM 依据自旋的成功率,如果自旋的成功率高,那么接着自旋,如果自旋获取锁的成功率比拟低,比拟耗费资源,进入重量级锁。
锁降级流程的标准答案
如果面试官问锁降级的流程,我认为规范的答复如下:
- 在 JDK 8 到 14 是由无锁 转 偏差锁 自适应锁, 所谓自适应锁指的是 JVM 会依据运行过程中收集的信息来决定自旋还是阻塞线程,如果自旋取得锁的成功率比拟高,那么就是由偏差锁降级到轻量级锁。如果自旋获取锁的失败率比拟高,代表单个线程持有锁的工夫很长,那么 JVM 就会从轻量级锁转为重量级锁。
- 在 JDK 15 移除了偏差锁,起因在于引入偏差锁,次要是为了优化 JDK 1.0 的那两个汇合相干的代码,然而当初看来这两个汇合很少有人用到,况且 JVM 撤销偏差锁状态比拟耗费资源,所以 JDK 15 撤销了偏差锁。所以 JDK 15 的锁降级流程为 无锁到 轻量级锁 再到 重量级锁。
写在最初
其实本来打算写一下锁降级,然而锁降级牵扯到一个 safe point,而要介绍 safe point 又跟 GC 混在一起,原本打算介绍,然而起初想了一下,一篇介绍 safe point 不大事实。
参考文档
- JEP 374: Deprecate and Disable Biased Locking https://openjdk.org/jeps/374
- 深度剖析:锁降级过程和锁状态,看完这篇你就懂了!https://segmentfault.com/a/11…
- 不可不说的 Java“锁”事 https://tech.meituan.com/2018…