性能文章>死磕synchronized四:系统剖析偏向锁篇一>

死磕synchronized四:系统剖析偏向锁篇一原创

1794017

哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。

 

手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO…

 

 

近期准备写一个专栏:从Hotspot源码角度剖析synchronized。前前后后大概有10篇,会全网发,写完后整理成电子书放公众号供大家下载。对本专栏感兴趣的、希望彻彻底底学明白synchronized的小伙伴可以关注一波。电子书整理好了会通过公众号群发告知大家。我的公众号:硬核子牙。

 

从本篇文章开始,给大家分享我对偏向锁的研究成果。写多少篇不确定,篇幅长度不确定,唯一确定的就是要把偏向锁前前后后讲透彻。

 

01

偏向锁

synchronized刚开始引入偏向锁的时候,我就觉得很奇怪:轻量级锁已经是应用态的锁了,为什么还要搞一个偏向锁,后面花了很长时间研究这个问题,并找到了答案。BTW,CAS是基于lock指令实现的,这个指令不会引起态的切换,即不会引起应用态切内核态。

 

看官方怎么说:

偏置锁定是 HotSpot 虚拟机中使用的一种优化技术,用于减少无竞争锁定的开销。它旨在避免在获取监视器时执行比较和交换原子操作,方法是假设监视器一直归给定线程所有,直到不同的线程尝试获取它。监视器的初始锁定使监视器 偏向 于该线程,从而避免在对同一对象的后续同步操作中需要原子指令。当许多线程对以单线程方式使用的对象执行许多同步操作时,与常规锁定技术相比,偏置锁历来会导致显着的性能改进。

 

jdk15开始,开始考虑默认关闭偏向锁,由业务方决定是否开启。具体看官方怎么说:

过去看到的性能提升今天远没有那么明显。许多受益于偏向锁定的应用程序是使用早期 Java 集合 API 的较旧的遗留应用程序,这些 API 在每次访问时进行同步(例如,Hashtable和Vector)。较新的应用程序通常使用非同步集合(例如,HashMap和ArrayList),在 Java 1.2 中引入用于单线程场景,或者在 Java 5 中引入用于多线程场景的更高性能的并发数据结构。这意味着如果更新代码以使用这些较新的类,由于不必要的同步而受益于偏向锁定的应用程序可能会看到性能改进。
此外,围绕线程池队列和工作线程构建的应用程序通常在禁用偏置锁定的情况下性能更好。(SPECjbb**** 就是这样设计的,例如,而 SPECjvm98 和 SPECjbb2005 则不是)。
偏向锁定带来了在争用情况下需要昂贵的撤销操作的成本。因此,受益于它的应用程序只有那些表现出大量无竞争同步操作的应用程序,如上面提到的那些,因此,执行廉价的锁所有者检查加上偶尔的昂贵撤销的成本仍然低于执行躲避的比较和交换原子指令的成本。自从将偏向锁定引入 HotSpot 以来,原子指令成本的变化也改变了该关系保持真实所需的无竞争操作的数量。另一个值得注意的方面是,即使在之前的成本关系是正确的情况下,当花费在同步操作上的时间仍然只占整个应用程序工作负载的一小部分时,应用程序也不会从偏向锁定中获得明显的性能改进。
偏向锁定在同步子系统中引入了大量复杂的代码,并且对其他 HotSpot 组件也具有侵入性。这种复杂性是理解代码各个部分的障碍,也是在同步子系统内进行重大设计更改的障碍。为此,我们希望禁用、弃用并最终删除对偏向锁定的支持。

 

总结一下就是说:

一、目前大多数Java程序都不会使用那些使用了synchronized的类库如Hashtable、Vector,而是用无锁的类库,需要锁的时候自己加锁;

二、偏向锁撤销的成本很高,需要在安全点下才能干净地完成。言外之意就是撤销偏向锁时,需要STW;

三、偏向锁的代码很复杂,又侵入了其他业务分支,导致代码难以理解难以维护难以拓展。具体哪些地方会用到,后面会讲到。

 

原文链接:https://openjdk.java.net/jeps/374

 

02

会讲哪些内容

在synchronized对应的几种锁类型中,偏向锁是最难的:

  1. synchronized有两种用法:修饰方法、代码段

  2. 入口有两处:一、模板解释器那里通过汇编实现了;二、fast_enter

  1. fast_enter被很多地方调用,针对偏向锁需要做不同的处理,导致代码逻辑分支特别多

  2. 偏向锁逻辑需要同时处理三种锁状态:无锁、未偏向的偏向锁,已偏向的偏向锁

  1. 偏向锁逻辑还需要兼顾撤销及竞争膨胀成轻量级锁

  2. 撤销或重偏向太过频繁,还会触发批量撤销与重偏向

  1. 多个线程同时强占偏向锁如何处理

  2. 占用偏向锁的线程执行结束如何处理

  1. hashcode、wait对锁的影响

  2. 一些我没想到的

 

后面的文章,就是针对这些问题或场景进行分享。

 

本篇文章聚焦分析synchronize修饰方法的情况下偏向锁的工作机制,下篇文章从Hotspot源码角度给出分析。再下篇分析synchronized代码段情况下偏向锁的工作机制。

 

03

偏向锁如何研究

我最开始研究偏向锁的时候,就一个感觉:难、绕、累。就那么几个方法,来来回回看了很多遍,能搜到的资料也基本都看了,说能背出来都不为过,每段代码做什么的也一清二楚,但是与代码测试的结果还是对不上。我特么奔溃了,好家伙,遇到硬茬了,激起了我满满的斗志。

 

我要开始分析了。一般研究一个东西,脑力消耗特别大,又研究不出结果,我就会思考是不是我的研究方式出现了问题。然后我就搜索我的三十六计锦囊,组合出了这套研究套路:一、要画堆栈图,因为偏向锁会用到栈中的lock record;二、什么类型的锁会进入偏向锁逻辑;三、针对多线程来说,要考虑三种情况:某个线程一直持有锁、多个线程交替持有锁、多个线程竞争锁。

 

先说下第二个问题,只有匿名偏向锁及偏向锁会进入偏向锁代码逻辑。无锁、轻量级锁、重量级锁,都会被各种条件判断拦截跳出。进入以后干什么事呢?要么撤销、要么重偏向、要么膨胀,还有,要么恢复到无锁状态。如果update_heuristics方法被调用的次数过于频繁,会触发批量撤销及批量重偏向。

 

接下来详细说下偏向锁的整体逻辑:

  1. 如果是匿名偏向锁状态,单线程环境下,CAS肯定成功,拿到偏向锁,安安稳稳的执行完,释放偏向锁。注意这里是偏向锁的释放,可能很多小伙伴都不知道,偏向锁还有释放逻辑。

  2. 如果是匿名偏向锁状态,多个线程竞争,CAS成功的线程拿到偏向锁,CAS失败的线程就会触发偏向锁撤销及膨胀成轻量级锁

  1. 如果是偏向锁状态,这种情况跟上面讲的CAS失败的线程走的代码逻辑,基本上是一样的,都是去抢占别的线程已经占有的锁。所以这时候就需要用到安全点,在STW环境下撤销、膨胀。这就是经常说的竞争环境下偏向锁性能差的原因。注意这里,膨胀成轻量级锁,拿到这个轻量级锁的还是原来那个持有偏向锁的线程,而不是来抢锁的线程。抢锁的线程会继续触发膨胀成重量级锁。还有一种情况,就是持有偏向锁的线程已经over,这时候撤销成什么状态得看是否想重新偏向,如果想,撤销为匿名偏向,不想,撤销为无锁。

 

看完以后,知道大家有很多疑惑,别急,下篇细讲。

 

BTW,安全点的可怕不是从开启安全点到代码执行完解除安全点花费了时间,真正的耗时还有加上开启安全点到所有线程进入安全点阻塞这段时间,通常这个时间占总耗时的比重比较大,理解了这个你再看安全点插的位置就能深刻理解了。言外之意,STW的总耗时=开启安全点到所有线程进入安全点进入阻塞状态花费的时间+接触安全点到唤醒所有线程恢复运行花费的时间。

 

04

偏向锁堆栈图

堆栈图要考虑这两种情况:当前是synchronized修饰方法还是synchronized代码段,这两种情况对应的堆栈图是完全不一样的。看有些文章说基本差不多,好扯。

 

本篇文章聚焦分析synchronized修饰方法,考虑三种情况:单个线程持有、重入、其他线程抢占偏向锁。分别针对这三种情况给出堆栈图,后面分析撤销、释放偏向锁都要用到。有了这个图,也才能更好地理解Hotspot源码。

 

如果是单个线程进入或者单个线程重复进入,堆栈图如下

 

 

如果是多线程环境下,那你的脑海中得有两个虚拟机栈。堆栈图如下

 

 

本篇文章就到这里,下篇文件开始从Hotspot源码层面讲解偏向锁。下篇文章见。

 

05

系列文章

1、JVM如何执行synchronized修饰的方法

2、系统剖析延迟偏向篇一

3、系统剖析延迟偏向篇二

 

06

推荐阅读

1、技术人如何才能拿到百万年薪?

2、为什么他们成为了技术大牛?

3、深入剖析Lambda表达式的底层实现原理

 

题外话

 

你好,我是子牙。十余年技术生涯,一路披荆斩棘从小白到技术总监到大厂中间件到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核及特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。不考虑交个朋友吗?关注硬核子牙:

分类:标签:
请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得