java 中的 synchronized 运行

在 Java 中,我们经常用 synchronized 关键字对程序进行加锁。无论是一个代码块还是静态方法或者实例方法,都可以直接用 synchronized 声明。

当声明 synchronized 代码块时,编译的字节码将包含 monitorenter 和 monitorexit 指令。这两种指令均会消耗操作数栈上的一个引用类型的元素,作为所要加锁解锁的锁对象。

  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

上述代码以及字节码中,包含了一个 monitorenter 指令以及多个 monitorexit 指令。这是因为 Java 虚拟机需要确保所获得的锁在正常执行以及异常执行的路径上都能够被解锁。

关于 monitorenter 和 monitorexit 的作用,可以抽象理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行 monitorenter 时,如果目标对象的计数器为 0,那么说明它没有加锁。这个时候,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。如果目标对象的计数器不为 0,判断该锁对象的持有线程是否为当前线程,如果说,则计数器加 1。否则需要等待,直至持有线程释放该锁。

当执行 monitorexit 时,Java 虚拟机则需要将对象的计数器减 1。当计数器值为 0 时,代表该锁已经被释放掉了。

之所以采用这种计数器的方式,是为了允许同一线程重复获取同一把锁。例如:一个 Java 类中拥有多个 synchronized 方法,那么这些方法之间互相调用,无论直接或间接,都会涉及对同一把锁的重复加锁操作。

接下来总结 HotSpot 虚拟机中具体的锁实现。

重量级锁

重量级锁是 Java 虚拟机中最为基础的锁实现。这种情况下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。

Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的。这些操作涉及系统的调用,需要从操作系统的用户态切换至内核态,其开销非常之大。为了尽量避免昂贵的线程阻塞,唤醒操作,Java 虚拟机会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并且伦旭锁是否被释放。与阻塞状态相比,自旋状态会浪费大量的处理器资源。

举例:以等红绿灯为例,Java 线程的阻塞相当于熄火停车,自旋状态相当于怠速停车。如果红灯时间长,那么熄火停车更胜油。如果红灯时间段,怠速停车更加适合。

对于 Java 虚拟机来说,并不能看到红灯的剩余时间(不能明确知道线程保持自旋状态多久可以加锁)。这时,Java 虚拟机给出可一种自适应的方案,根据以往自旋等待时是否获得锁,来动态调整自旋的时间。

举例:上次没熄火就等到了绿灯,这次就把怠速停车的时间设置久一点。上次没熄火没有等到绿灯,这次就把怠速停车时间设置短一点。

自旋状态还有一个副作用,那便是不公平的锁机制。处于阻塞状态的线程,并没有办法立刻竞争被释放的锁。而处于自选状态的线程,则可能有限获得这把锁。

轻量级锁

深夜的十字路口,车辆来往很少,可能会出现一个路口一辆车在等红绿灯,这样的话车辆通行效率太低。于是,路口的灯设置成黄灯,过往车辆通过路口时注意避让,最后保证依次通过。

Java 虚拟机也存在类似的情形:多个线程在不同的时间段请求通一把锁,不存在锁竞争。针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。

Java 虚拟机是这样区分轻量级锁和重量级锁的。在对象头中的标记字段,最后两位用来表示该对象的锁状态。00 代表轻量级锁,01 代表无锁(或偏向锁),10 代表重量级锁,11 代表跟垃圾回收算法的标记有关。

当加锁时,Java 虚拟机会判断是否是重量锁。如果不死,会在当前线程的当前栈帧中划出一块空间,作为锁的锁记录,并且将锁对象的标记字段复制到该锁记录中。

之后,Java 虚拟机会尝试用 CAS 操作替换锁对象的标记字段。CAS 是一个原子操作,它会比较目标地址的值是否和期望值相等,如果相等,则替换为一个新的值。

举例:当前锁对象的标记字段为 X-XYZ,Java 迅疾会比较该字段是否为 X-X01。如果是,则替换为刚才分配的锁记录的地址。此时,该线程已成功获得这把锁,可以继续执行了。如果不是 X-X01,那么分两种情况:第一,该线程重复获取通一把锁。此时,Java 虚拟机会将锁记录清零,以代表该锁被重复获取。第二,其他线程持有该锁。此时,Java 虚拟机将把锁膨胀为重量级锁,并且阻塞当前线程。

当解锁时,如果当前锁记录的值为 0,则代表重复进入同一把锁,直接返回即可。否则,Java 虚拟机会尝试用 CAS 操作,比较锁对象的标记字段的值是否为当前锁记录的地址。如果是,则替换为锁记录中的值,也就是锁对象原本的标记字段。此时,该线程成功释放这把锁。如果不是,则意味着这把锁已经膨胀为重量级锁。此时 Java 虚拟机会进入重量级锁的释放过程,唤醒因竞争该锁二倍阻塞了的线程。

偏向锁

轻量级锁针对的是乐观的情况,而偏向锁针对就是更加乐观的情况:从始至终只有一个线程请求某一把锁。

如同红路灯路口一直是红灯,当看到你的车来的时候,红灯才会变成绿灯,其他车一概都是红灯,禁止通行。

当加锁的时候,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中,并且标记字段的最后三位设置为 101。

接下来的运行过程中,每当有线程请求这把锁,Java 虚拟机只需判断锁兑现标记的字段中,最后三位是否为 101,是否包含当前线程的地址,以及 epoch 值是否和锁对象的类的 epoch 值相同。如果满足,那么当前线程持有该偏向锁,可以直接返回。

什么是 epoch

先从偏向锁的撤销讲起。当请求加锁的线程和锁对象标价字段保持的线程地址不匹配时,Java 虚拟机需要撤销该偏向锁。这个撤销过程要求持有偏向锁的线程到达安全点,再讲偏向锁替换成轻量级锁。

如果某一类锁对象的总撤销数超过了一个阈值(相关参数的值为:20),那么 Java 虚拟机会宣布这个类的偏向锁失效。

具体的做法,在每一个类中维护一个 epoch 值,可以理解为第几代偏向锁。当设置偏向锁时,Java 虚拟机需要将该 epoch 值复制到锁对象的标记字段中。

在宣布某个类的偏向锁失效是,Java 虚拟机实则将该类的 epoch 值加 1,表示之前那一代的偏向锁已经失效。而新设置的偏向锁则需要复制新的 epoch 值。

为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java 虚拟机需要遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将他们标记字段中的 epoch 值加 1。该操作需要线程出游安全点状态。如果总撤销数超过另一个阈值(40),此后的加锁过程中直接为该类实例设置轻量级锁。

总结

本文创作灵感来源于 极客时间 郑雨迪老师的《深入拆解 Java 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。

14 Java虚拟机实现 synchronized的更多相关文章

  1. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  2. Java基础之Synchronized原理

    思维导图svg: https://note.youdao.com/ynoteshare1/index.html?id=eb05fdceddd07759b8b82c5b9094021a&type ...

  3. Java并发之Synchronized机制详解

    带着问题阅读 1.Synchronized如何使用,加锁的粒度分别是什么 2.Synchronized的实现机制是什么 3.Synchronized是公平锁吗 4.Java对Synchronized做 ...

  4. 深入理解Java虚拟机(第三版)-14. 线程安全与锁优化

    14. 线程安全与锁优化 1. 什么是线程安全? 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替进行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个 ...

  5. Java虚拟机14:类加载器

    类与类加载器 虚拟机设计团队把类加载阶段张的"通过一个类的全限定名来获取此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类.实现这 ...

  6. 《深入Java虚拟机学习笔记》- 第14章 浮点运算

    <深入Java虚拟机学习笔记>- 第13章 浮点运算

  7. 《深入理解Java虚拟机》类文件结构

    上节学习回顾 在上一节当中,主要以自己的工作环境简单地介绍了一下自身的一些调优或者说是故障处理经验.所谓百变不离其宗,这个宗就是我们解决问题的思路了. 本节学习重点 在前面几章,我们宏观地了解了虚拟机 ...

  8. [转载] 深入理解Android之Java虚拟机Dalvik

    本文转载自: http://blog.csdn.net/innost/article/details/50377905 一.背景 这个选题很大,但并不是一开始就有这么高大上的追求.最初之时,只是源于对 ...

  9. Java虚拟机学习(5):类加载器(ClassLoader

    类加载器 类加载器(ClassLoader)用来加载 class字节码到 Java 虚拟机中.一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源文件在经过 Javac之后就被转换成 ...

随机推荐

  1. Vultr VPS建站攻略 – 一键安装LNMP无面板高性能WEB环境

    在"Vultr VPS建站攻略 - 一键安装宝塔面板架设LNMP/LAMP Web环境"文章中,VULTR中文网分享到我们常用的可视化面板宝塔面板安装在VULTR VPS主机中建站 ...

  2. [solr 管理界面] - 索引数据删除

    删除solr索引数据,使用XML有两种写法: 1) <delete><id>1</id></delete> <commit/> 2) < ...

  3. flush caches

  4. Luogu [P2814] 家谱

    题目链接 这个题不难,但是有点小小坑. 首先并查集肯定能看出来. 然后字符串的话,一开始我想用 hash 来处理,但想了想,离散化不好搞,人也太多了,一不小心就hash重了,还是算了. 然后就想到了S ...

  5. bootstrap2文档的学习

    就像刚开始的 优雅,直观,强大的前端框架,让web开发更快,更容易,bootstrap给我的感觉就是把常用的布局,组件(导航,列表,按钮,表格),还有规范化颜色等等,同时它的遍历不至于此,他还支持了自 ...

  6. icon踩坑记录

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. Java十进制转成二进制 八进制 十六进制

    int a = 357;//十进制转成二进制System.out.println(Integer.toBinaryString(a)); package com.swift; import java. ...

  8. Spring Cloud 入门 Eureka-Server服务注册

    这里就不介绍怎么创建springboot项目了,可以查看我前面的博客 Spring Cloud Eureka Spring Cloud Eureka是Spring Cloud Netflix项目下的服 ...

  9. python之斐波纳契数列

    斐波纳契数列 斐波那契数列指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,676 ...

  10. MSBuild常用方法

    打包后把nuget包复制到指定的目录 <Target Name="CopyPackage" AfterTargets="Pack"> <Cop ...