深入拆解Java虚拟机
郑雨迪
Oracle 高级研究员,计算机博士
立即订阅
27794 人已学习
课程目录
已完结 39 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 为什么我们要学习Java虚拟机?
免费
模块一:Java虚拟机基本原理 (12讲)
01 | Java代码是怎么运行的?
02 | Java的基本类型
03 | Java虚拟机是如何加载Java类的?
04 | JVM是如何执行方法调用的?(上)
05 | JVM是如何执行方法调用的?(下)
06 | JVM是如何处理异常的?
07 | JVM是如何实现反射的?
08 | JVM是怎么实现invokedynamic的?(上)
09 | JVM是怎么实现invokedynamic的?(下)
10 | Java对象的内存布局
11 | 垃圾回收(上)
12 | 垃圾回收(下)
模块二:高效编译 (12讲)
【工具篇】 常用工具介绍
13 | Java内存模型
14 | Java虚拟机是怎么实现synchronized的?
15 | Java语法糖与Java编译器
16 | 即时编译(上)
17 | 即时编译(下)
18 | 即时编译器的中间表达形式
19 | Java字节码(基础篇)
20 | 方法内联(上)
21 | 方法内联(下)
22 | HotSpot虚拟机的intrinsic
23 | 逃逸分析
模块三:代码优化 (10讲)
24 | 字段访问相关优化
25 | 循环优化
26 | 向量化
27 | 注解处理器
28 | 基准测试框架JMH(上)
29 | 基准测试框架JMH(下)
30 | Java虚拟机的监控及诊断工具(命令行篇)
31 | Java虚拟机的监控及诊断工具(GUI篇)
32 | JNI的运行机制
33 | Java Agent与字节码注入
模块四:黑科技 (3讲)
34 | Graal:用Java编译Java
35 | Truffle:语言实现框架
36 | SubstrateVM:AOT编译框架
尾声 (1讲)
尾声 | 道阻且长,努力加餐
深入拆解Java虚拟机
登录|注册

14 | Java虚拟机是怎么实现synchronized的?

郑雨迪 2018-08-22
在 Java 程序中,我们可以利用 synchronized 关键字来对程序进行加锁。它既可以用来声明一个 synchronized 代码块,也可以直接标记静态方法或者实例方法。
当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter 和 monitorexit 指令。这两种指令均会消耗操作数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里的引用),作为所要加锁解锁的锁对象。
public void foo(Object lock) {
synchronized (lock) {
lock.hashCode();
}
}
// 上面的Java代码将编译为下面的字节码
public void foo(java.lang.Object);
Code:
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: aload_1
5: invokevirtual java/lang/Object.hashCode:()I
8: pop
9: aload_2
10: monitorexit
11: goto 19
14: astore_3
15: aload_2
16: monitorexit
17: aload_3
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
我在文稿中贴了一段包含 synchronized 代码块的 Java 代码,以及它所编译而成的字节码。你可能会留意到,上面的字节码中包含一个 monitorenter 指令以及多个 monitorexit 指令。这是因为 Java 虚拟机需要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁。
你可以根据我在介绍异常处理时介绍过的知识,对照字节码和异常处理表来构造所有可能的执行路径,看看在执行了 monitorenter 指令之后,是否都有执行 monitorexit 指令。
当用 synchronized 标记方法时,你会看到字节码中方法的访问标记包括 ACC_SYNCHRONIZED。该标记表示在进入该方法时,Java 虚拟机需要进行 monitorenter 操作。而在退出该方法时,不管是正常返回,还是向调用者抛异常,Java 虚拟机均需要进行 monitorexit 操作。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《深入拆解Java虚拟机》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(42)

  • 谢阳
    如果不是 X…X01,那么有两种可能。第一,该线程重复获取同一把锁。此时,Java 虚拟机会将锁记录清零,以代表该锁被重复获取。第二,其他线程持有该锁。此时,Java 虚拟机会将这把锁膨胀为重量级锁,并且阻塞当前线程。

    老师这段不太明白。1 锁记录清零怎么理解?改变锁对象的标记字段吗?2 锁膨胀的时候其他线程还持有锁对象吧,这个时候膨胀会具体做什么操作?如果操作了锁对象的标记字段会影响稍后释放锁的cas吗
    2018-08-23
    14
  • 爪哇夜未眠
    太抽象了,老师能画点儿图吗……

    作者回复: 不好意思哈,因为网上有很多图,忘了放个链接了。

    你可以参考wiki.openjdk.java.net/display/HotSpot/Synchronization中的图。

    2018-08-22
    13
  • godtrue
    恩,今天才补上小结,因为听不明白了,后来反复听以及补上锁的相关知识才有点明白。
    我认为雨迪确实应该补上点图,这样才更容易理解,否则确实抽象,另外,我觉得讲解的次序有点小问题。
    如果这样讲就更容易理解了(个人见解)
    1:讲解一下锁的本质,锁到底是个什么东西?锁的特点容易理解,毕竟都见过摸过用过

    2:讲解一下锁的分类和特点,什么表锁、行锁、自旋锁、可重用锁、轻量锁、重量锁、阻塞锁、线程锁、进程锁、分布式锁、偏向锁等等吧!都是站在不同的角度或层级根据锁的特点,为了好区分给锁起的名字

    3:讲解一下JVM中的各种锁,讲解一下他们的特点和实现,然后讲解一下咱们本节的主角是属于哪一种或哪几种锁

    4:我的理解,锁的本质-在程序世界里是一种保证资源正确竞争的机制,如果没有对同一资源竞争也就没有了锁存在的意义,在计算世界中资源引起竞争的核心基本是空间,有其是计算机的内存空间,当然数据肯定也是一种引起激烈竞争的资源,不过往往会体现到空间上去,因为计算机中的数据必定存于某空间地址之中的

    5:感觉明白可重用锁的实现原理了,这个也是雨迪讲的最细致的一种实现方式,恩,非常感谢🙏

    作者回复: 多谢建议!

    本文的讲解流程是从通用锁算法到针对特殊情况的锁算法来的。一开始monitorenter是用重型锁的,然后为了针对没有竞争锁的情况有了轻型锁,再然后为了针对只有一个线程持有某个锁的情况有了偏向锁。

    2018-09-01
    10
  • QlDoors
    练习试了无数遍,都没有偏向锁,后来上网查才发现需要加-XX:BiasedLockingStartupDelay=0。

    http://zhizus.com/2018-09-03-%E5%81%8F%E5%90%91%E9%94%81.html

    注意:Hotspot虚拟机在开机启动后有个延迟(4s),经过延迟后才会对每个创建的对象开启偏向锁。我们可以通过设置下面的参数来修改这个延迟,或者直接sleep一段时间-XX:BiasedLockingStartupDelay=0
    2018-11-21
    7
  • code-artist
    “当进行解锁操作时,如果当前锁记录(你可以将一个线程的所有锁记录想象成一个栈结构,每次加锁压入一条锁记录,解锁弹出一条锁记录,当前锁记录指的便是栈顶的锁记录)的值为 0,则代表重复进入同一把锁,直接返回即可。”
    这种情况也需要弹出当前锁记录的吧? 不然锁记录一直是0不变了。 如果是我这样理解的话,重复获取同一把锁的话,不是简单地清零,而应该是把0作为一条新的锁记录压入栈顶。
    不知道我这样理解对不?请老师指点

    作者回复: 对的!赞

    2018-08-26
    6
  • 贾智文
    文中说轻量级锁因为内存对齐所以标识位是00,那么为什么重量级锁的时候,存储内容也是指针,却没有内存对齐呢?
    2018-08-22
    3
  • NEO🍋
    老师关于偏向锁有个疑问 “它针对的是锁仅会被同一线程持有的情况。” 如果只有一个线程持有锁 还有必要加锁吗?

    作者回复: 哈,这个属于应用程序的问题,JVM只是观察到这种情况,并尝试做出优化。

    有一种可能,就是很长一段时间内,只有一个线程频繁加锁,后面换成另外的线程,这样前面那段时间可以用偏向锁。

    2018-08-22
    3
  • 加载中……
    您好,文章写的挺好,读完有个问题想请教下:
    当t1线程获取了某个对象锁(lock1)的偏向锁,还没执行完的时候,另外一个线程t2也尝试获取这个对象锁(lock1),我看文章上说需要撤销偏向锁,等到达安全点的时候,再将偏向锁替换成轻量级锁。
    我有个问题:两个线程同时竞争同一把锁的情况,轻量级锁也解决不了吧,只能用重量级锁解决吧?为什么还要替换成轻量级锁呢?
    2019-02-26
    2
  • Geek_987169
    老师请教个问题:
    1:锁从偏向一直到重量级的过程是"单向不可逆"的,这个"单向不可逆"是限制在对象的整个生命周期,还是在对象到达了某个状态后再次有线程使用其作为锁对象还会继续重复这个过程?从每撤销一次对象的epoch值就会+1,而这个+1代表的就是偏向锁升级为轻量级锁,而每个对象又维护了一个epoch值代表对象撤销次数(偏向锁->轻量级锁次数),是不是就代表这个锁升级的过程会在不同的时间段重复发生n词?
    2:为什么要设置一个最大的撤销次数(epoch值),意义在哪里?

    作者回复: 1. 单向不可逆 针对一个对象的整个生命周期。

    epoch+1发生在多次同一类型的实例的偏向锁撤销之后,存放在类型(Class)那里的。

    2. 当频繁检测到某个类的实例出现撤销偏向锁的,就代表这个类不适合用来搞偏向锁。

    2018-10-18
    2
  • 何yuan
    一直认为synchronized是重量锁,是否也不一定?jvm处理的时候是先将当偏向锁处理,然后慢慢膨胀为重量级锁的是吗?

    作者回复: 默认情况下是的。以前有个延缓毫秒数-XX:BiasedLockingStartupDelay,一开始用轻量级锁,在启动四秒之后才开始用偏向锁。我记得Java 9还是10默认值改为0了。

    2018-08-22
    2
  • ゞ、今生绝恋丶
    雨迪老师,我有个疑问:假设线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!我觉得我理解的哪块不对?
    2019-04-08
    1
    1
  • Geek_987169
    重复获取同一把锁,锁记录清零是什么意思?如果将锁记录理解为栈结构,那么如果一个线程重复获取同一把锁这个此时锁记录有几条,是什么结构呢?
    2018-10-18
    1
  • javaadu
    你好,例子运行了,没看出啥不同,只有“fast path lock entries”这个对应的数值不同,slow path lock entries一直是2,其他的都是0。

    建议作者给出自己的实验截图,方便我们对比
    2018-08-24
    1
  • Scott
    请教一下,不是很明白epoch的作用,偏向锁默认被撤销20次使偏向锁失效才更新这个字段,没有想明白epoch的必要性
    2018-08-22
    1
  • (^o^)
    “否则,Java 虚拟机会尝试用 CAS 操作,比较锁对象的标记字段的值是否为当前锁记录的地址。如果是,则替换为锁记录中的值,也就是锁对象原本的标记字段。此时,该线程已经成功释放这把锁。

    如果不是,则意味着这把锁已经被膨胀为重量级锁。”升级为重量级锁的过程,会改变锁对象的什么信息呢?
    2018-08-22
    1
  • (^o^)
    Synchronized某个对象时,这个对象的锁先是偏向锁,后根据具体竞争情况先升级为轻量级锁再升级为重量级锁吗?
    2018-08-22
    1
  • 长脖子树
    关于 monitorenter / monitorexit 两种指令均会消耗操作数栈上的一个引用类型的元素 我解释下
    这两个指令在执行前均需要将锁的对象引用推至栈顶
    顺便分享下本节的思维导图哈哈
    https://www.processon.com/view/link/5dccb8a1e4b071befcc7edb7
    2019-11-14
  • -W.LI-
    老师好有个问题。调用notify all时。锁还是被调用线程持有着的,只有出了临界区才会释放锁。被唤醒的那些线程会从等待队列进入锁池队列。在notify all 和出临界区这段时间内,被唤醒的线程,会去竞争锁么?还是只有除了临界区释放锁后才会触发锁竞争。我看有本书上说notify all写在临界区末尾,可减少锁竞争。
    2019-10-06
  • 码农Kevin亮
    请问老师,如何分析什么情况下是哪种锁?以及如何验证分析结论呢
    2019-09-20
  • nidafg
    感觉老师讲的很学院派,如果能有图文或者更多比喻就好了
    2019-09-10
收起评论
42
返回
顶部