synchronized底层实现原理详解(新手必看)
synchronized 的中文意思是“同步”,在 Java 中它是一种同步锁,可用于解决多个线程之间访问资源的同步问题。
当代码块或方法被 synchronized 修饰时,可以保证在任意时刻只有一个线程执行这些被修饰的方法或代码块,其作用是保障程序在并发执行时的正确性,主要体现在以下 3 方面:
synchronized 关键字的底层是通过锁对象关联的监视器(Monitor)实现的,每个对象都有一个关联的 Monitor,那 Monitor 到底是什么呢?
我们可以把 Monitor 理解为一个同步工具或一种同步机制,但它通常被描述为一个对象。在 Java 中,一切皆对象,并且所有的 Java 对象都是天生的 Monitor,每个 Java 对象都有成为 Monitor 的潜质,因为在 Java 的设计中,每一个 Java 对象从初始化起就带了一把看不见的锁,它叫作内部锁或 Monitor 锁。
每个对象都存在一个Monitor与之关联,对象与其 Monitor 之间的关系又存在多种实现方式,比如 Monitor 可以与对象一起创建和销毁,也可以在线程试图获取对象锁时自动生成,但当一个 Monitor 被某个线程持有后,它便处于锁定状态。
在 JVM(HotSpot)中,Monitor 是由 ObjectMonitor 实现的,位于 HotSpot 虚拟机源码的 ObjectMonitor.hpp 文件中,具体使用 C++ 实现,其主要数据结构如下:
_WaitSet、_cxq 与 _EntryList 都采用链表结构:
当多个线程竞争 Monitor 对象时,所有没有竞争到的线程会被封装成 ObjectWaiter 并加入 _EntryList 列表。当一个已经获取锁的线程调用锁对象的 wait() 方法失去锁后,线程会被封装成一个 ObjectWaiter 并加入 _WaitSet 列表中。当线程调用锁对象的 notify() 方法后,会根据情况将 _WaitSet 列表中的元素转移到 _cxq 列表或 _EntryList 列表,等到获取锁的线程释放锁后,再根据条件来执行该方法。
当遇到多个线程同步处理时,ObjectMonitor 状态变化如下:
为了便于大家理解上述过程,我们整理了相应的流程图,如下图所示。

图 1
了解 Monitor 锁之后,接下来我们进一步探究一下 synchronized 底层的实现原理。

图 2
从上图所示的字节码信息可知,synchronized 修饰代码块的实现原理是使用了 monitorenter 和 monitorexit 指令:
通过对 monitorenter 和 monitorexit 这两个指令的描述,我们应该能很清楚地看出 synchronized 关键字的实现原理,其中,monitorenter 指令指明同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,当前线程将试图获取 objectref 对象所关联的 Monitor 的所有权,如果 Monitor 计数器的值为 0,那么线程可以成功取得 Monitor,并将计数器的值加 1,取锁成功。
如果当前线程已经拥有了 objectref 对象关联的 Monitor 的所有权,那么它可以重入这个 Monitor,重入时计数器的值会加 1。若其他线程已经拥有 objectref 对象关联的 Monitor 所有权,那么当前线程将被阻塞,直到持有的线程执行完毕,即 monitorexit 指令会被执行,此时线程将释放 Monitor,并将 Monitor 计数器的值减 1,当 Monitor 计数器的值为 0 时 ,其他线程将有机会获得 Monitor。
值得我们注意的是,编译器会确保线程方法执行完毕后释放持有的 Monitor,无论方法是正常结束还是异常结束的,因此方法中调用过的每一条 monitorenter 指令都会执行其对应的 monitorexit 指令。为了保证在方法异常结束时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它用来执行 monitorexit 指令。
从图 2 中我们可以看出多了一条 monitorexit 指令,它就是在异常结束时释放 Monitor 的指令。
另外,wait() 和 notify() 等方法也是依赖于 Monitor 对象的,这也是它们只能在 synchronized 代码块或方法中使用,否则程序会抛出 java.lang.IllegalMonitorStateException 异常的原因。
然后使用 javac 和 javap 命令,获得反编译后的字节码信息,具体如下图所示:

图 3
synchronized 修饰方法的实现原理是隐式的,不需要使用 monitorenter 和 monitorexit 字节码指令来控制,它是在方法调用和返回操作之中实现的。
JVM 从方法常量池中的方法表结构(method_info Structure)信息判断该方法是否使用 synchronized 修饰,在方法调用时,调用指令会检查方法的 flags 是否被设置了 ACC_SYNCHRONIZED 标志,如果标志进行了相应设置,执行线程需要先持有 Monitor,然后才能执行方法,最后在方法无论是正常结束还是异常结束时释放 Monitor。
在方法执行期间,执行线程持有了 Monitor,其他任何线程都无法再获得同一 个Monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的 Monitor 将在异常抛到同步方法之外时自动释放。
从图 3 所示的字节码信息可以看出,synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标志,该标志指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
当代码块或方法被 synchronized 修饰时,可以保证在任意时刻只有一个线程执行这些被修饰的方法或代码块,其作用是保障程序在并发执行时的正确性,主要体现在以下 3 方面:
- 保证程序可见性:保证共享变量的修改能够及时可见;
- 保证程序原子性:确保线程互斥地访问同步代码;
- 保证程序有序性:有效解决指令重排序问题。
synchronized 关键字的底层是通过锁对象关联的监视器(Monitor)实现的,每个对象都有一个关联的 Monitor,那 Monitor 到底是什么呢?
我们可以把 Monitor 理解为一个同步工具或一种同步机制,但它通常被描述为一个对象。在 Java 中,一切皆对象,并且所有的 Java 对象都是天生的 Monitor,每个 Java 对象都有成为 Monitor 的潜质,因为在 Java 的设计中,每一个 Java 对象从初始化起就带了一把看不见的锁,它叫作内部锁或 Monitor 锁。
每个对象都存在一个Monitor与之关联,对象与其 Monitor 之间的关系又存在多种实现方式,比如 Monitor 可以与对象一起创建和销毁,也可以在线程试图获取对象锁时自动生成,但当一个 Monitor 被某个线程持有后,它便处于锁定状态。
在 JVM(HotSpot)中,Monitor 是由 ObjectMonitor 实现的,位于 HotSpot 虚拟机源码的 ObjectMonitor.hpp 文件中,具体使用 C++ 实现,其主要数据结构如下:
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0; _Responsible = NULL; _succ = NULL; _cxq = NULL; FreeNext = NULL; _EntryList = NULL; _SpinFreq = 0; _SpinClock = 0; OwnerIsThread = 0; }ObjectMonitor 中有 5 个重要部分,分别为 _owner、_WaitSet、_cxq、_EntryList 和 _count:
- _owner:用来指向持有 Monitor 对象的线程,它的初始值为 NULL,表示当前没有任何线程获取锁。当一个线程成功获取锁之后会保存线程的 ID,等到线程释放锁后 _owner 又会被重置为 NULL。
- _WaitSet:当一个已获取锁的线程调用了锁对象的 wait() 方法后,线程会被加入这个列表中。
- _cxq:一个阻塞的单向线程列表,线程被唤醒后根据决策判断放入 _cxq 还是 _EntryList。
- _EntryList:当多个线程竞争 Monitor 对象时,没有竞争到的线程会被加入这个列表。
- _count:用于记录线程获取锁的次数,成功获取到锁后 _count 会加 1,释放锁时 _count 会减 1。
_WaitSet、_cxq 与 _EntryList 都采用链表结构:
- _WaitSet 存放的是处于等待状态的线程;
- _EntryList 存放的是处于等待锁状态的线程;
- _cxq 列表中的线程只临时存放,最终会被转移到 _EntryList 中等待获取锁。
当多个线程竞争 Monitor 对象时,所有没有竞争到的线程会被封装成 ObjectWaiter 并加入 _EntryList 列表。当一个已经获取锁的线程调用锁对象的 wait() 方法失去锁后,线程会被封装成一个 ObjectWaiter 并加入 _WaitSet 列表中。当线程调用锁对象的 notify() 方法后,会根据情况将 _WaitSet 列表中的元素转移到 _cxq 列表或 _EntryList 列表,等到获取锁的线程释放锁后,再根据条件来执行该方法。
当遇到多个线程同步处理时,ObjectMonitor 状态变化如下:
- 多个线程在同时访问一段同步代码时,首先会进入 _EntryList 列表,当线程获取到对象的 Monitor 后才进入 _owner 区域,并把 Monitor 中的 _owner 变量设置为当前线程,同时 Monitor 中锁的 _count 加 1。
- 当线程调用 wait() 方法时,会释放当前持有的 Monitor,_owner 变量恢复为 NULL,_count 自动减 1,同时该线程进入 _WaitSet 列表中等待被唤醒。
- 当线程执行完毕后,会释放 Monitor 并复位 _count 变量的值,以便其他线程进入并获取 Monitor。
为了便于大家理解上述过程,我们整理了相应的流程图,如下图所示。

图 1
了解 Monitor 锁之后,接下来我们进一步探究一下 synchronized 底层的实现原理。
synchronized修饰代码块的原理
我们首先写一段使用 synchronized 修饰代码块的测试程序,具体代码如下:public class SynchronizedDemo1 { public void method() { synchronized(this) { System.out.println("synchronized 修饰代码块"); } } }然后使用 javac 命令编译上述代码生成 .class 文件,再使用 javap 命令反编译得到字节码,具体如下图所示:

图 2
从上图所示的字节码信息可知,synchronized 修饰代码块的实现原理是使用了 monitorenter 和 monitorexit 指令:
-
monitorenter指令:每个对象有一个监视器(Monitor),当 Monitor 被占用时它就会处于锁定状态。线程执行 monitorenter 指令时会尝试获取 Monitor 的所有权,具体过程如下:
- 如果 Monitor 的进入数为 0,则该线程获取 Monitor,然后将进入数设置为 1,该线程即 Monitor 的所有者。
- 如果线程已经占有该 Monitor,只是重新进入它,则 Monitor 的进入数加 1。
- 如果其他线程已经占用了 Monitor,则该线程进入阻塞状态,直到 Monitor 计数器的值为 0,再重新尝试获取 Monitor 的所有权。
- monitorexit指令:执行 monitorexit 的线程必须是 objectref 对象关联的 Monitor 的所有者。monitorexit 指令执行时,Monitor 计数器的值减 1。如果减 1 后计数器的值为 0,则该线程退出 Monitor,不再是这个 Monitor 的所有者。其他被这个 Monitor 阻塞的线程可以尝试去获取这个 Monitor 的所有权。
通过对 monitorenter 和 monitorexit 这两个指令的描述,我们应该能很清楚地看出 synchronized 关键字的实现原理,其中,monitorenter 指令指明同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,当前线程将试图获取 objectref 对象所关联的 Monitor 的所有权,如果 Monitor 计数器的值为 0,那么线程可以成功取得 Monitor,并将计数器的值加 1,取锁成功。
如果当前线程已经拥有了 objectref 对象关联的 Monitor 的所有权,那么它可以重入这个 Monitor,重入时计数器的值会加 1。若其他线程已经拥有 objectref 对象关联的 Monitor 所有权,那么当前线程将被阻塞,直到持有的线程执行完毕,即 monitorexit 指令会被执行,此时线程将释放 Monitor,并将 Monitor 计数器的值减 1,当 Monitor 计数器的值为 0 时 ,其他线程将有机会获得 Monitor。
值得我们注意的是,编译器会确保线程方法执行完毕后释放持有的 Monitor,无论方法是正常结束还是异常结束的,因此方法中调用过的每一条 monitorenter 指令都会执行其对应的 monitorexit 指令。为了保证在方法异常结束时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它用来执行 monitorexit 指令。
从图 2 中我们可以看出多了一条 monitorexit 指令,它就是在异常结束时释放 Monitor 的指令。
另外,wait() 和 notify() 等方法也是依赖于 Monitor 对象的,这也是它们只能在 synchronized 代码块或方法中使用,否则程序会抛出 java.lang.IllegalMonitorStateException 异常的原因。
synchronized修饰方法的原理
我们首先写一段使用 synchronized 修饰方法的测试程序,具体代码如下:public class SynchronizedDemo2 { public synchronized void method() { System.out.println("synchronized 修饰方法"); } }
然后使用 javac 和 javap 命令,获得反编译后的字节码信息,具体如下图所示:

图 3
synchronized 修饰方法的实现原理是隐式的,不需要使用 monitorenter 和 monitorexit 字节码指令来控制,它是在方法调用和返回操作之中实现的。
JVM 从方法常量池中的方法表结构(method_info Structure)信息判断该方法是否使用 synchronized 修饰,在方法调用时,调用指令会检查方法的 flags 是否被设置了 ACC_SYNCHRONIZED 标志,如果标志进行了相应设置,执行线程需要先持有 Monitor,然后才能执行方法,最后在方法无论是正常结束还是异常结束时释放 Monitor。
在方法执行期间,执行线程持有了 Monitor,其他任何线程都无法再获得同一 个Monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的 Monitor 将在异常抛到同步方法之外时自动释放。
从图 3 所示的字节码信息可以看出,synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标志,该标志指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
相关文章
- Java synchronized底层实现原理详解(新手必看)
- Java synchronized可重入锁的底层实现(新手必看)
- Java synchronized实现线程同步
- Java synchronized实现线程互斥(附带实例)
- Java synchronized:实现线程同步(附带实例)
- Java synchronized关键字是怎样保证线程安全的?
- Java synchronized锁升级过程和原理(新手必看)
- Java synchronized锁的粗化和消除(新手必看)
- Java volatile和synchronized的区别(新手必看)
- Java值传递和引用传递的区别(附带实例)