相关文章:
在 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
-
由字节码可知,同步代码块的实现使用的是
monitorenter
和monitorexit
指令-
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 对象 -
为了保证在方法异常完成时,
monitorenter
和monitorexit
指令依然可以配对正确执行,编译器会自动产生一个异常处理器,这个异常处理器声明可以处理所有的异常,它的目的就是用来执行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
-
由字节码可知,同步方法的字节码中并没有
monitorenter
和monitorexit
指令,这是因为方法级别的同步是隐式的,即无需通过字节码指令来控制,而是通过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 同步代码块由
monitorenter
和monitorexit
两个指令来实现,其中monitorenter
指向同步代码块的开始位置,monitorexit
指向同步代码块的结束位置 -
当执行
monitorenter
指令时,当前线程试图获取锁 (Monitor 对象) 的持有权,如果此时计数器为 0,则可以成功获取,并将计数器的值设为 1,表示取锁成功;若获取失败,则表示该锁 (Monitor 对象) 已经被其他线程所持有,那么当前线程将被阻塞,直到锁被另一个线程释放为止 -
当执行
monitorexit
指令时,当前线程会释放锁 (Monitor 对象) 的持有权,并将计数器的值设为 0,同时唤醒被阻塞的其他线程
-
-
synchronized 同步方法
-
synchronized 同步方法由
ACC_SYNCHRONIZED
访问标志来实现,没有使用monitorenter
和monitorexit
指令 -
虚拟机通过
ACC_SYNCHRONIZED
访问标志来区分一个方法是否为同步方法,进而执行相应的同步调用
-
-
-
synchronized 的优化
-
自适应自旋锁 (Adaptive Spinning)
-
锁消除 (Lock Eliminate)
-
锁粗化 (Lock Coarsening)
-
轻量级锁 (LightWeight Locking)
-
偏向锁 (Biased Locking)
-