14 Java虚拟机实现 synchronized

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 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。

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

原文地址:https://www.cnblogs.com/yuepenglei/p/10331327.html

时间: 2024-10-09 21:27:15

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

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

14. 线程安全与锁优化 1. 什么是线程安全? 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替进行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的 2. Java语言中的线程安全 我们将Java语言下的线程安全分为以下五类:不可变.绝对线程安全.相对线程安全.线程兼容和线程对立. 1.不可变:不可变一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要进行任何线程安全保障措施

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

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

深入了解Java虚拟机(3)类文件结构

虚拟机执行子系统 一.类文件结构 1.魔数和class版本 1.magic-魔数:0xCAFEBABE:4字节 2.minor_version:次版本,丶之后的数字:2字节 3.major_version:主版本,丶之前的数字:2字节 2.常量池 1.constant_pool_count:常量池常量数量(= 此值 - 1):2字节 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值. 2.constant_pool:常量,第一位为类型位,之后的

[转]深入理解java虚拟机 精华总结(面试)

原文 http://www.cnblogs.com/prayers/p/5515245.html 一.运行时数据区域 3 1.1 程序计数器 3 1.2 Java虚拟机栈 3 1.3 本地方法栈 3 1.4 Java堆 3 1.5 方法区 3 1.6 运行时常量池 4 二. hotspot虚拟机对象 4 2.1 对象的创建 4 1. 检查 4 2. 分配内存 4 3. Init 4 2.2 对象的内存布局 4 2.3 对象的访问定位 4 1. 使用句柄访问 4 2. 使用直接指针访问 5 三. 

读《深入理解Java虚拟机》

Java虚拟机运行时数据区 对象的创建 Java创建对象,在语言层面上使用new关键字.虚拟机遇到new关键字时,会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载.解析和初始化过.如果没有,那就必须先执行类加载过程.类加载通过之后,虚拟机将会为新生对象分配内存.对象所需的内存在类加载完成后就能完全确定.分配内存的方法有"指针碰撞"和"空闲列表"两种方式,如果Java堆是规整的,则采用前者:否则,采用后者.Java

Java并发编程 Synchronized及其实现原理

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法.Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题. Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 1.普通同步方法,锁是当前实例对象 public class SynchronizedTest { 4 public synchronized void method1(){ 5 System

深入理解java虚拟机 精华总结(面试)

一.运行时数据区域 3 1.1 程序计数器 3 1.2 Java虚拟机栈 3 1.3 本地方法栈 3 1.4 Java堆 3 1.5 方法区 3 1.6 运行时常量池 4 二. hotspot虚拟机对象 4 2.1 对象的创建 4 1. 检查 4 2. 分配内存 4 3. Init 4 2.2 对象的内存布局 4 2.3 对象的访问定位 4 1. 使用句柄访问 4 2. 使用直接指针访问 5 三. OutOfMemoryError 异常 5 3.1 Java堆溢出 5 3.2 虚拟机栈和本地方法

java 多线程8 : synchronized锁机制 之 方法锁

脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的.注意这里 局部变量是不存在脏读的情况 多线程线程实例变量非线程安全 看一段代码: public class ThreadDomain13 { private int num = 0; public void addNum(String userName) { try { if ("

读《深入理解Java虚拟机》有感——第二部分:虚拟机类加载机制

一.类加载过程 执行时机:编译程序——>执行程序(JVM启动.程序运行),类加载发生在程序运行期间 各个阶段:分为加载阶段.连接阶段(验证.准备.解析).初始化.使用.卸载 执行顺序:大体是按以上阶段依次执行,但相互间有交叉                      加载——>验证(文件格式)——>继续加载——>解析——>验证(元数据.字节码)——>准备——>初始化 参与角色:Class文件.Java虚拟机.类加载器  /**HotSpot的Bootstrap C