Lock -- 02 -- synchronized底层实现原理

原文链接:Lock – 02 – synchronized底层实现原理


相关文章:


在 Java 虚拟机中,同步是基于进入和退出管程 (Monitor) 对象来实现的,每个 Java 对象天生自带了一把看不见的锁,这个即为 Monitor 锁 (也称为管程或监视器锁),我们可以把它理解成一个同步工具,也可以描述为一种同步机制,通常它被描述为一个对象

在此之前,我们需要先来了解一下 Java 对象头,其主要有以下三个部分

  • Mark Word

    • Mark Word 用于存储对象自身的运行时数据,如:哈希码 (HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等

    • 32 位 Mark Word

      在这里插入图片描述

    • 64 位 Mark Word

      在这里插入图片描述

  • 类型指针

    • 类型指针即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例
  • 数组长度

    • 当存储的是普通 Java 对象时,数组长度是非必需的,虚拟机可以通过普通 Java 对象的元数据信息来确定普通 Java 对象的大小

    • 当存储的是数组对象时,数组长度是必需的,因为如果数组长度不确定的话,虚拟机将无法通过数组对象的元数据来确定数组对象的大小

了解完对象头之后,我们主要来分析一下重量级锁,也就是通常说的 synchronized 的对象锁


一、重量级锁 (synchronized 的对象锁)

  • 重量级锁,锁标识位为 10,其中指针指向的是 Monitor 对象的起始地址,每个对象都存在一个 Monitor 对象与之关联,对象与其 Monitor 对象之间存在多种实现方式,如:Monitor 对象可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 Monitor 对象被某个线程持有后,它便处于锁定状态

  • 在 Java 虚拟机中,Monitor 是由 ObjectMonitor 来实现的,其源码如下,参考地址 --> objectMonitor.hpp

    • ObjectMonitor

      在这里插入图片描述

      • _WaitSet

        • 等待池,用于存放那些主动放弃锁标记,等待别的线程来唤醒的线程
      • _EntryList

        • 锁池,用于存放那些想要拿到对象的锁标记却没有拿到的线程
    • _WaitSet_EntryList 用于保存 ObjectWaiter 对象列表,每个等待锁的线程都会被封装成 ObjectWaiter 对象

      • ObjectWaiter

        在这里插入图片描述

    • 此外,_owner 用于指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码块时,首先会进入 _EntryList 集合里,当线程获取到对象的 Minitor 对象后会进入 _Owner 区域,并把 Minitor 对象中的 _owner 变量设置为当前线程,同时将 _count 变量加 1

    • 若线程调用 wait() 方法,将释放当前所持有的 Monitor 对象,并将 _owner 变量恢复为 null,同时将 _count 变量减 1,然后将该线程放入 _WaitSet (等待池) 中等待被唤醒

    • 若线程执行完毕,也将释放所持有的 Minitor 对象,并复位对应变量的值,以边其他线程进入获取 Minitor 对象

    • ObjectMonitor 的结构如下所示

      在这里插入图片描述

    • 由此可见,Monitor 对象存在于每个 Java 对象的对象头中 (存储的指针),synchronized锁便是通过这种方式来获取锁的,这也是为什么 Java 中任意对象都可以作为锁的原因,下面我们将进一步分析 synchronized 在字节码层面的具体语义分析


二、字节码语义分析

  • SyncBlockAndMethod.java

    public class SyncBlockAndMethod {
    
        public void syncBlockTask() {
            synchronized (this) {
                System.out.println("Hello");
            }
        }
    
        public synchronized void syncMethodTask() {
            System.out.println("Hello Again");
        }
    }
    
  • 如上所示,我们编写了一个测试类,并用 javac 对其进行编译,获取其 Class 文件,再使用 javap -verbose 来获取字节码的具体信息,如下所示

    Classfile /D:/MyWorkspace/idea-myproject-maven/springboot-test/src/main/java/com/test/SyncBlockAndMethod.class
      Last modified 2020-4-25; size 625 bytes
      MD5 checksum 39fc6799f8bc75ac6b53f3756c4ea849
      Compiled from "SyncBlockAndMethod.java"
    public class com.test.SyncBlockAndMethod
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #23            // Hello
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = String             #26            // Hello Again
       #6 = Class              #27            // com/test/SyncBlockAndMethod
       #7 = Class              #28            // java/lang/Object
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               syncBlockTask
      #13 = Utf8               StackMapTable
      #14 = Class              #27            // com/test/SyncBlockAndMethod
      #15 = Class              #28            // java/lang/Object
      #16 = Class              #29            // java/lang/Throwable
      #17 = Utf8               syncMethodTask
      #18 = Utf8               SourceFile
      #19 = Utf8               SyncBlockAndMethod.java
      #20 = NameAndType        #8:#9          // "<init>":()V
      #21 = Class              #30            // java/lang/System
      #22 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
      #23 = Utf8               Hello
      #24 = Class              #33            // java/io/PrintStream
      #25 = NameAndType        #34:#35        // println:(Ljava/lang/String;)V
      #26 = Utf8               Hello Again
      #27 = Utf8               com/test/SyncBlockAndMethod
      #28 = Utf8               java/lang/Object
      #29 = Utf8               java/lang/Throwable
      #30 = Utf8               java/lang/System
      #31 = Utf8               out
      #32 = Utf8               Ljava/io/PrintStream;
      #33 = Utf8               java/io/PrintStream
      #34 = Utf8               println
      #35 = Utf8               (Ljava/lang/String;)V
    {
      public com.test.SyncBlockAndMethod();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 9: 0
    
      public void syncBlockTask();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter
             4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             7: ldc           #3                  // String Hello
             9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            12: aload_1
            13: monitorexit
            14: goto          22
            17: astore_2
            18: aload_1
            19: monitorexit
            20: aload_2
            21: athrow
            22: return
          Exception table:
             from    to  target type
                 4    14    17   any
                17    20    17   any
          LineNumberTable:
            line 12: 0
            line 13: 4
            line 14: 12
            line 15: 22
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 17
              locals = [ class com/test/SyncBlockAndMethod, class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    
      public synchronized void syncMethodTask();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #5                  // String Hello Again
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 18: 0
            line 19: 8
    }
    SourceFile: "SyncBlockAndMethod.java"
    
  • 我们先来分析 syncBlockTask() 方法,其部分字节码如下所示

    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
    
    • 由字节码可知,同步代码块的实现使用的是 monitorentermonitorexit 指令

      • monitorenter

        • 指向同步代码块的开始位置
      • monitorexit

        • 指向同步代码块的结束位置
    • 当执行 monitorenter 指令时,当前线程将试图获取对象锁 (ObjectRef (Object Reference)) 所对应的 Monitor 对象的持有权,当 ObjectRef 对应的 Monitor 对象的计数器 (_count) 为 0 时,那线程就可以成功地获取到 Monitor 对象,并将计数器 (_count) 的值设为 1,表示取锁成功

    • 如果当前线程已经拥有了 ObjectRef 的 Monitor 对象的持有权,那它可以重入这个 Monitor 对象,重入时计数器 (_count) 的值也会加 1

      • 重入

        • 当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态

        • 但当一个线程再次请求自己持有的对象锁的临界资源时,这种情况属于重入

      • 在 Java 中 synchronized 基于原子性的内部锁机制,是可重入的,因此在一个线程调用 synchronized 方法的同时,在其方法体内再调用该对象的另一个 synchronized 方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是被允许的

    • 如果其他线程已经拥有了 ObjectRef 的 Monitor 对象的持有权,那么当前线程将被阻塞,直到正在执行的线程执行完毕,即执行了 monitorexit 指令。当执行完毕后,会释放 Monitor 对象,并将计数器 (_count) 的值设为 0,其他线程将有机会持有 Monitor 对象

    • 为了保证在方法异常完成时,monitorentermonitorexit 指令依然可以配对正确执行,编译器会自动产生一个异常处理器,这个异常处理器声明可以处理所有的异常,它的目的就是用来执行 monitorexit 指令,从字节码中也可以看出多了一个 monitorexit 指令,它就是异常结束时被执行的释放 Monitor 对象的指令

  • 接着我们再来分析 syncMethodTask() 方法,其部分字节码如下所示

    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String Hello Again
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
    
    • 由字节码可知,同步方法的字节码中并没有 monitorentermonitorexit 指令,这是因为方法级别的同步是隐式的,即无需通过字节码指令来控制,而是通过 ACC_SYNCHRONIZED 访问标志来区分一个方法是否为同步方法

    • 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,则执行线程将先持有 Monitor 对象,然后再执行方法,最后在方法完成 (无论是正常完成还是非正常完成) 时释放 Monitor 对象。在方法完成期间,执行线程持有了 Monitor 对象,其他任何线程都无法再获得同一个 Monitor 对象

    • 如果一个同步方法在执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的 Monitor 对象将在异常被抛到同步方法之外时自动释放


三、synchronized 的优化

  • 在早期的 Java 版本中,synchronized 属于重量级锁,效率低下,依赖于底层的操作系统的 Mutex Lock 来实现的,而操作系统实现线程之间的切换时,需要从用户态切换到内核态,这个状态之间的转换需要较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因

  • 不过在 JDK6 以后,在 JVM 层面对 synchronized 做了很大的优化,引入了许多技术,如:自适应自旋锁 (Adaptive Spinning)锁消除 (Lock Eliminate)锁粗化 (Lock Coarsening)轻量级锁 (Lightweight Locking)偏向锁 (Biased Locking),这些技术都是为了在线程之间更高效地功效数据,以及解决竞争问题,从而提高程序的执行效率


四、归纳总结

  • synchronized 底层实现原理

    • synchronized 底层实现原理属于 JVM 层面,由 Monitor 对象 (每个对象存在一个 Monitor 对象与之关联) 来实现,synchronized 为重量级锁,同步对象的对象头中的 Mark Word 的锁标识位为 10,Mark Word 中存储的是指向重量级锁的指针,即指向的是 Monitor 对象的起始地址

    • synchronized 同步代码块

      • synchronized 同步代码块由 monitorentermonitorexit 两个指令来实现,其中 monitorenter 指向同步代码块的开始位置,monitorexit 指向同步代码块的结束位置

      • 当执行 monitorenter 指令时,当前线程试图获取锁 (Monitor 对象) 的持有权,如果此时计数器为 0,则可以成功获取,并将计数器的值设为 1,表示取锁成功;若获取失败,则表示该锁 (Monitor 对象) 已经被其他线程所持有,那么当前线程将被阻塞,直到锁被另一个线程释放为止

      • 当执行 monitorexit 指令时,当前线程会释放锁 (Monitor 对象) 的持有权,并将计数器的值设为 0,同时唤醒被阻塞的其他线程

    • synchronized 同步方法

      • synchronized 同步方法由 ACC_SYNCHRONIZED 访问标志来实现,没有使用 monitorentermonitorexit 指令

      • 虚拟机通过 ACC_SYNCHRONIZED 访问标志来区分一个方法是否为同步方法,进而执行相应的同步调用

  • synchronized 的优化

    • 自适应自旋锁 (Adaptive Spinning)

    • 锁消除 (Lock Eliminate)

    • 锁粗化 (Lock Coarsening)

    • 轻量级锁 (LightWeight Locking)

    • 偏向锁 (Biased Locking)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ReentrantLocksynchronized都是用于实现并发编程中的同步机制,但它们的底层原理和使用方式有所不同。 1. synchronized底层原理synchronized是Java中的关键字,它基于进入和退出监视器对象(monitor)来实现方法同步和代码块同步。在Java对象头中,有一个标志位用于表示对象是否被锁定。当线程进入synchronized代码块时,它会尝试获取对象的锁,如果锁已经被其他线程持有,则该线程会被阻塞,直到锁被释放。当线程退出synchronized代码块时,它会释放对象的锁,使其他线程可以获取锁并执行相应的代码。 2. ReentrantLock底层原理: ReentrantLock是Java中的一个类,它使用了一种称为CAS(Compare and Swap)的机制来实现同步。CAS是一种无锁的同步机制,它利用了CPU的原子指令来实现对共享变量的原子操作。ReentrantLock内部维护了一个同步状态变量,通过CAS操作来获取和释放锁。当一个线程尝试获取锁时,如果锁已经被其他线程持有,则该线程会进入等待状态,直到锁被释放。与synchronized不同,ReentrantLock提供了更灵活的锁获取和释放方式,例如可以实现公平锁和可重入锁。 总结: - synchronized是Java中的关键字,基于进入和退出监视器对象来实现同步,而ReentrantLock是一个类,使用CAS机制来实现同步。 - synchronized是隐式锁,不需要手动获取和释放锁,而ReentrantLock是显式锁,需要手动调用lock()方法获取锁,unlock()方法释放锁。 - ReentrantLock相比synchronized更灵活,可以实现公平锁和可重入锁等特性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值