大家好, 关于偏向锁,这篇从底层角度进行剖析,相信会给大家带来不一样的启发。
关于并发编程,悟空之前也写过几篇,有些读者朋友们专门搜出来看了,我在这里汇总下,方便大家查阅。
[1、读了源码,发现 volatile 如此重要!]
[2、程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?]
[3、用积木讲解ABA原理]
[4、21 张图 | 带你领略集合的 线程不安全]
[5、5.000字 | 24张图带你彻底理解Java中的21种锁]
[6、详解 18 种队列,你知道几种?]
[7、16 道多线程面试题 | 水帘洞版(免费下载)]
另外也可以到我的个人网站上查看。
www.passjava.cn
在 JDK1.5 之前,面对 Java 并发问题, synchronized 是一招鲜的解决方案:
拿同步块来举例:
public void test(){
synchronized (object) {
i++;
}
}
经过 javap -v
编译后的指令如下:
monitorenter
指令是在编译后插入到同步代码块的开始位置;monitorexit
是插入到方法结束和异常的位置(实际隐藏了try-finally),每个对象都有一个 monitor 与之关联,当一个线程执行到 monitorenter 指令时,就会获得对象所对应的 monitor
的所有权,也就获得到了对象的锁
当另外一个线程执行到同步块的时候,由于它没有对应 monitor
的所有权,就会被阻塞,此时控制权只能交给操作系统,也就会从 user mode
切换到 kernel mode
, 由操作系统来负责线程间的调度和线程的状态变更, 需要频繁的在这两个模式下切换(上下文转换)。这种有点竞争就找内核的行为很不好,会引起很大的开销,所以大家都叫它重量级锁,自然效率也很低,这也就给很多童鞋留下了一个根深蒂固的印象 —— synchronized关键字相比于其他同步机制性能不好
来到 JDK1.6,要怎样优化才能让锁变的轻量级一些?答案就是:
如果 CPU 通过简单的 CAS 能处理加锁/释放锁,这样就不会有上下文的切换,较重量级锁而言自然就轻了很多。但是当竞争很激烈,CAS 尝试再多也是浪费 CPU,权衡一下,不如升级成重量级锁,阻塞线程排队竞争,也就有了轻量级锁升级成重量级锁的过程
程序员在追求极致的道路上是永无止境的,HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,同一个线程反复获取锁,如果还按照轻量级锁的方式获取锁(CAS),也是有一定代价的,如何让这个代价更小一些呢?
偏向锁实际就是锁对象潜意识「偏心」同一个线程来访问,让锁对象记住线程 ID,当线程再次获取锁时,亮出身份,如果同一个 ID 直接就获取锁就好了,是一种 load-and-test
的过程,相较 CAS 自然又轻量级了一些
可是多线程环境,也不可能只是同一个线程一直获取这个锁,其他线程也是要干活的,如果出现多个线程竞争的情况,也就有了偏向锁升级的过程
这里可以先思考一下:偏向锁可以绕过轻量级锁,直接升级到重量级锁吗?
都是同一个锁对象,却有多种锁状态,其目的显而易见:
占用的资源越少,程序执行的速度越快
偏向锁,轻量锁,它俩都不会调用系统互斥量(Mutex Lock),只是为了提升性能,多出的两种锁的状态,这样可以在不同场景下采取最合适的策略,所以可以总结性的说:
到这里,大家应该理解了全局大框,但仍然会有很多疑问:
想理解这些问题,需要先知道 Java 对象头的结构
按照常规理解,识别线程 ID 需要一组 mapping 映射关系来搞定,如果单独维护这个 mapping 关系又要考虑线程安全的问题。奥卡姆剃刀原理,Java 万物皆是对象,对象皆可用作锁,与其单独维护一个 mapping 关系,不如中心化将锁的信息维护在 Java 对象本身上
Java 对象头最多由三部分构成:
MarkWord
其中 Markword
是保存锁状态的关键,对象锁状态可以从偏向锁升级到轻量级锁,再升级到重量级锁,加上初始的无锁状态,可以理解为有 4 种状态。想在一个对象中表示这么多信息自然就要用位
存储,在 64 位操作系统中,是这样存储的(注意颜色标记),想看具体注释的可以看 hotspot(1.8) 源码文件 path/hotspot/src/share/vm/oops/markOop.hpp
第 30 行
有了这些基本信息,接下来我们就只需要弄清楚,MarkWord 中的锁信息是怎么变化的
单纯的看上图,还是显得十分抽象,作为程序员的我们最喜欢用代码说话,贴心的 openjdk 官网提供了可以查看对象内存布局的工具 JOL (java object layout)
Maven Package
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
Gradle Package
implementation 'org.openjdk.jol:jol-core:0.14'
接下来我们就通过代码来深入了解一下偏向锁吧
注意:
上图(从左到右) 代表
高位 -> 低位
JOL 输出结果(从左到右)代表
低位 -> 高位
来看测试代码
public static void main(String[] args) {
Object o = new Object();
log.info("未进入同步块,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
来看输出结果:
上面我们用到的 JOL 版本为 0.14
, 带领大家快速了解一下位具体值,接下来我们就要用 0.16
版本查看输出结果,因为这个版本给了我们更友好的说明,同样的代码,来看输出结果:
看到这个结果,你应该是有疑问的,JDK 1.6 之后默认是开启偏向锁的,为什么初始化的代码是无锁状态,进入同步块产生竞争就绕过偏向锁直接变成轻量级锁了呢?
虽然默认开启了偏向锁,但是开启有延迟,大概 4s。原因是 JVM 内部的代码有很多地方用到了synchronized,如果直接开启偏向,产生竞争就要有锁升级,会带来额外的性能损耗,所以就有了延迟策略
我们可以通过参数 -XX:BiasedLockingStartupDelay=0
将延迟改为0,但是不建议这么做。我们可以通过一张图来理解一下目前的情况:
那我们就代码延迟 5 秒来创建对象,来看看偏向是否生效
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
log.info("未进入同步块,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
重新查看运行结果:
这样的结果是符合我们预期的,但是结果中的 biasable
状态,在 MarkWord 表格中并不存在,其实这是一种匿名偏向状态,是对象初始化中,JVM 帮我们做的
这样当有线程进入同步块:
那问题又来了,现在锁对象有具体偏向的线程,如果新的线程过来执行同步块会偏向新的线程吗?
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
log.info("未进入同步块,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
Thread t2 = new Thread(() -> {
synchronized (o) {
log.info("新线程获取锁,MarkWord为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
}
});
t2.start();
t2.join();
log.info("主线程再次查看锁对象,MarkWord为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("主线程再次进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
来看运行结果,奇怪的事情发生了:
标记1
: 初始可偏向状态标记2
:偏向主线程后,主线程退出同步代码块标记3
: 新线程进入同步代码块,升级成了轻量级锁标记4
: 新线程轻量级锁退出同步代码块,主线程查看,变为不可偏向状态标记5
: 由于对象不可偏向,同场景1主线程再次进入同步块,自然就会用轻量级锁至此,场景一二三可以总结为一张图:
从这样的运行结果上来看,偏向锁像是“一锤子买卖”,只要偏向了某个线程,后续其他线程尝试获取锁,都会变为轻量级锁,这样的偏向非常有局限性。事实上并不是这样,如果你仔细看标记2(已偏向状态),还有个 epoch 我们没有提及,这个值就是打破这种局限性的关键,在了解 epoch 之前,我们还要了解一个概念——偏向撤销
在真正讲解偏向撤销之前,需要和大家明确一个概念——偏向锁撤销和偏向锁释放是两码事
何为偏向撤销?
从偏向状态撤回原有的状态,也就是将 MarkWord 的第 3 位(是否偏向撤销)的值,
从 1 变回 0
如果只是一个线程获取锁,再加上「偏心」的机制,是没有理由撤销偏向的,所以偏向的撤销只能发生在有竞争的情况下
想要撤销偏向锁,还不能对持有偏向锁的线程有影响,所以就要等待持有偏向锁的线程到达一个 safepoint 安全点
(这里的安全点是 JVM 为了保证在垃圾回收的过程中引用关系不会发生变化设置的一种安全状态,在这个状态上会暂停所有线程工作), 在这个安全点会挂起获得偏向锁的线程
在这个安全点,线程可能还是处在不同状态的,先说结论(因为源码就是这么写的,可能有疑惑的地方会在后面解释)
这个和 epoch 貌似还是没啥关系,因为这还不是全部场景。偏向锁是特定场景下提升程序效率的方案,可并不代表程序员写的程序都满足这些特定场景,比如这些场景(在开启偏向锁的前提下):
很显然,这两种场景肯定会导致偏向撤销的,一个偏向撤销的成本无所谓,大量偏向撤销的成本是不能忽视的。那怎么办?既不想禁用偏向锁,还不想忍受大量撤销偏向增加的成本,这种方案就是设计一个有阶梯的底线
这是第一种场景的快速解决方案,以 class 为单位,为每个 class 维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器 +1
,当这个值达到重偏向阈值(默认20)时:
BiasedLockingBulkRebiasThreshold = 20
JVM 就认为该class的偏向锁有问题,因此会进行批量重偏向, 它的实现方式就用到了我们上面说的 epoch
Epoch
,如其含义「纪元」一样,就是一个时间戳。每个 class 对象会有一个对应的epoch
字段,每个处于偏向锁状态对象的mark word
中也有该字段,其初始值为创建该对象时 class 中的epoch
的值(此时二者是相等的)。每次发生批量重偏向时,就将该值加1,同时遍历JVM中所有线程的栈
epoch
字段改为新值epoch
字段值不变这样下次获得锁时,发现当前对象的epoch
值和class的epoch
,本着今朝不问前朝事 的原则(上一个纪元),那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过 CAS 操作将其mark word
的线程 ID 改成当前线程 ID,这也算是一定程度的优化,毕竟没升级锁;
如果 epoch
都一样,说明没有发生过批量重偏向, 如果 markword
有线程ID,还有其他锁来竞争,那锁自然是要升级的(如同前面举的例子 epoch=0)
批量重偏向是第一阶梯底线,还有第二阶梯底线
当达到重偏向阈值后,假设该 class 计数器继续增长,当其达到批量撤销的阈值后(默认40)时,
BiasedLockingBulkRevokeThreshold = 40
JVM就认为该 class 的使用场景存在多线程竞争,会标记该 class 为不可偏向。之后对于该 class 的锁,直接走轻量级锁的逻辑
这就是第二阶梯底线,但是在第一阶梯到第二阶梯的过渡过程中,也就是在彻底禁用偏向锁之前,还给一次改过自新的机会,那就是另外一个计时器:
BiasedLockingDecayTime = 25000
[20, 40)
内的计数, 再给次机会大家有兴趣可以写代码测试一下临界点,观察锁对象 markword
的变化
至此,整个偏向锁的工作流程可以用一张图表示:
到此,你应该对偏向锁有个基本的认识了,但是我心中的好多疑问还没有解除,咱们继续看:
上面场景一,无锁状态,对象头中没有 hashcode;偏向锁状态,对象头还是没有 hashcode,那我们的 hashcode 哪去了?
首先要知道,hashcode 不是创建对象就帮我们写到对象头中的,而是要经过第一次调用 Object::hashCode()
或者System::identityHashCode(Object)
才会存储在对象头中的。第一次生成的 hashcode后,该值应该是一直保持不变的,但偏向锁又是来回更改锁对象的 markword,必定会对 hashcode 的生成有影响,那怎么办呢?,我们来用代码验证:
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
log.info("未生成 hashcode,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
log.info("已生成 hashcode,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
来看运行结果
结论就是:即便初始化为可偏向状态的对象,一旦调用
Object::hashCode()
或者System::identityHashCode(Object)
,进入同步块就会直接使用轻量级锁
假如已偏向某一个线程,然后生成 hashcode,然后同一个线程又进入同步块,会发生什么呢?来看代码:
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
log.info("未生成 hashcode,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
o.hashCode();
log.info("生成 hashcode");
synchronized (o){
log.info(("同一线程再次进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
查看运行结果:
结论就是:同场景一,会直接使用轻量级锁
那假如对象处于已偏向状态,在同步块中调用了那两个方法会发生什么呢?继续代码验证:
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
log.info("未生成 hashcode,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
log.info("已偏向状态下,生成 hashcode,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
来看运行结果:
结论就是:如果对象处在已偏向状态,生成 hashcode 后,就会直接升级成重量级锁
最后用书中的一段话来描述 锁和hashcode 之前的关系
Object 除了提供了上述 hashcode 方法,还有 wait()
方法,这也是我们在同步块中常用的,那这会对锁产生哪些影响呢?来看代码:
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
Thread.sleep(5000);
Object o = new Object();
log.info("未生成 hashcode,MarkWord 为:");
log.info(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
log.info(("进入同步块,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
log.info("wait 2s");
o.wait(2000);
log.info(("调用 wait 后,MarkWord 为:"));
log.info(ClassLayout.parseInstance(o).toPrintable());
}
}
查看运行结果:
结论就是,wait 方法是互斥量(重量级锁)独有的,一旦调用该方法,就会升级成重量级锁(这个是面试可以说出的亮点内容哦)
最后再继续丰富一下锁对象变化图:
看到这个标题你应该是有些慌,为啥要告别偏向锁,因为维护成本有些高了,来看 Open JDK 官方声明,JEP 374: Deprecate and Disable Biased Locking,相信你看上面的文字说明也深有体会,为了一个现在少有的场景付出了巨大的代码实现
这个说明的更新时间距离现在很近,在 JDK15 版本就已经开始了
一句话解释就是维护成本太高
最终就是,JDK 15 之前,偏向锁默认是 enabled,从 15 开始,默认就是 disabled,除非显示的通过 UseBiasedLocking 开启
其中在 quarkus 上的一篇文章说明的更加直接
偏向锁给 JVM 增加了巨大的复杂性,只有少数非常有经验的程序员才能理解整个过程,维护成本很高,大大阻碍了开发新特性的进程(换个角度理解,你掌握了,是不是就是那少数有经验的程序员了呢?哈哈)
偏向锁可能就这样的走完了它的一生,有些同学可能直接发问,都被 deprecated 了,JDK都 17 了,还讲这么多干什么?
之前对于偏向锁我也只是单纯的理论认知,但是为了写这篇文章,我翻阅了很多资料,包括也重新查看 Hotspot 源码,说的这些内容也并不能完全说明偏向锁的整个流程细节,还需要大家具体实践追踪查看,这里给出源码的几个关键入口,方便大家追踪:
文中有疑问的地方欢迎留言讨论,有错误的地方还请大家帮忙指正
感谢各路前辈的精华总结,可以让我参考理解:
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/tSxOeJxJIcjoc-l1uxPFmg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。