死磕synchronized二:系统剖析延迟偏向篇一原创
哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。
手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO…
近期准备写一个专栏:从Hotspot源码角度剖析synchronized。前前后后大概有10篇,会全网发,写完后整理成电子书放公众号供大家下载。对本专栏感兴趣的、希望彻彻底底学明白synchronized的小伙伴可以关注一波。电子书整理好了会通过公众号群发告知大家。我的公众号:硬核子牙。
市面上关于synchronized的资料已经很多了,我这个专栏跟那些资料有啥差别呢:
-
更系统。市面上目前虽然资料众多,但都是零散的。有些资料讲得东西甚至是相互冲突的,都不知道信谁的。我准备从Java层面到JVM层面到操作系统层面系统的去分析用synchronized后呈现的每个现象背后的本质。synchronized很多知识点市面上是没有资料讲的,我给它补上。
-
更接近真相。市面上的很多资料,有的是基于字节码解释器那块的代码yy出来的,有的是东拼西凑整合出来的,各个说的都像真的一样,把看的人搞蒙圈了。我准备从模板解释器代码入手,单步调试着研究,有些不确定的自己写代码去证明,争取分享给大家的都是本来如此的知识。不确定的地方我会标注出来。
-
授人以鱼不如授人以渔。我会以大家学完后能够手写出synchronized的标准来设计这个专栏。因为从我自己研究的角度来说,抛开语言的障碍,synchronized的每种机制如果让你实现你手足无措,那你还是没有真正地理解synchronized。言外之意就是你不一定要去手写,但是你在脑海中回想,比如CAS、锁膨胀、锁对象加锁解锁……你大概知道代码是怎么写的。
本篇文章是第二篇,聚焦分析偏向锁延迟策略:
-
什么是延迟偏向
-
为什么需要延迟偏向
-
延迟偏向机制是怎样的
-
延迟偏向对锁膨胀的影响及证明
-
从Hotspot源码角度证明
内容有点多,分两篇发。
什么是偏向延迟呢?见名知意:偏向锁就算是开启的,也不是马上就可以用的,中间有个延时。
对应的JVM参数是BiasedLockingStartupDelay,默认是4秒,可通过-XX:BiasedLockingStartupDelay修改
不知道大家在看到JVM中有偏向延迟这个机制的时候,脑海中有没有冒出这么几个问题:1、为什么要搞个偏向延迟?2、这个延时是从什么时候开始计算的?从JVM启动时吗?
为什么要设计
先回答第一个问题。这个问题的答案在网上有很多版本,最权威的答案就是这段注释。翻译过来就是说:这是一个启动时间回归的解决方案。说人话就是这样做,JVM启动可以更快。
为什么会更快呢?按照注释的说法:因为JVM在启动期间会采取大量安全点来消除偏差。这跟偏向锁有啥关系?说下我的理解哈,不一定是JVM工程师设计此的初衷。安全点大家应该是很熟悉了,启用安全点会带来STW。而偏向锁的撤销与重偏向判断,也是需要启用安全点的,因为需要扫描所有线程的虚拟机栈,需要内存静止才能保证结果准确。而JVM在启动期间用到的锁,包括初始化很多类的过程中用的锁,都会经过偏向锁逻辑,如果没有偏向延迟,就会带来更多的STW,导致JVM启动时间过长。
多说一句,有点不好理解:启动期间启用安全点消除偏差是一种先行发生策略,是为了保证启动期间有互动的多个线程的业务先后顺序。跟为了追求低延时数据同步插入内存屏障触发即时回写内存是差不多的思想。
延时何时计算
看下上面的代码,如果有延时,就创建一个任务。偏向延迟就是在这个任务中完成的,是由WatcherThread执行的,延迟偏向以后,WatcherThread执行到任务的task方法,创建一个VM_Operation丢入VMThread的任务池队列,等待VMThread执行。如果木有延时,就很直接了,创建一个VM_Operation丢人VMThread的任务池队列,等待VMThread执行。
顺便吐槽下,这个延时实现相当复杂。总之,Hotspot源码里面,就没有简单的东西。作为局外人,有些地方,我是真的不解,为啥要搞那么复杂。比如偏向锁整个机制,也是复杂的一批。
那延时是从哪开始的呢?是WatcherThread执行到sleep方法开始的,因为计算剩余时间一定需要与当前时间进行对比。
void WatcherThread::run() {
……
// Calculate how long it'll be until the next PeriodicTask work
// should be done, and sleep that amount of time.
int time_waited = sleep();
……
PeriodicTask::real_time_tick(time_waited);
}
synchronized对应的锁类型有这些:
-
无锁
-
偏向锁
-
轻量级锁
-
重量级锁
这些锁存储在哪个位置呢?对象头中。Java的每个对象,在JVM中的结构如图。Mark Word区域就是对象头。展开来就是上图的样子
好的答案从好的问题开始。对于这个问题,咱们从这几个问题开始着手:
-
延迟偏向之前创建的对象是什么锁
-
延迟偏向之后创建的对象是什么锁
-
创建的对象的锁是是如何被延迟偏向影响的。这个下篇讲
-
延迟偏向之后,之前创建的对象持有的锁会被批量修改吗
-
有或无延迟偏向,锁如何膨胀
延迟偏向之前创建的对象是无锁状态。细节下篇讲
延迟偏向之后创建的对象是未偏向的偏向锁。细节后面讲
延迟偏向之前创建的对象,延迟偏向后,是不会批量修改的。言外之意就是延迟偏向之前创建的对象是无锁,延迟偏向之后还是无锁。
延迟偏向之前是无锁,膨胀后不会经历偏向锁,会直接膨胀成轻量级锁
延迟偏向之后创建的对象是未偏向的偏向锁,经过synchronized就会变成偏向当前线程的偏向锁
关于synchronized的锁膨胀逻辑,后面写文章细讲。
我有话说
答应我,如果你学会了,不要拿这道题去为难别人好吗?如果你实在忍不住,不要告诉别人是子牙老师那边学来的。我不想承担无谓的仇恨:NND,这是哪个抽风的面试官想到的面试题!同是程序猿,相煎何太急!
看到这篇文章的小伙伴,如果你在哪里面试遇到了这道面试题,请留言告诉我。我再发几个声讨!我与罪恶不同戴天!
我是子牙。十余年技术生涯,一路披荆斩棘从小白到技术总监到大厂中间件到创业。技术栈如汇编、C语言、C++、Windows内核、Linux内核及特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。不考虑交个朋友吗?关注硬核子牙: