首页 > 编程笔记 > Java笔记 阅读:16

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++ 实现,其主要数据结构如下:
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:
_WaitSet、_cxq 与 _EntryList 都采用链表结构:
当多个线程竞争 Monitor 对象时,所有没有竞争到的线程会被封装成 ObjectWaiter 并加入 _EntryList 列表。当一个已经获取锁的线程调用锁对象的 wait() 方法失去锁后,线程会被封装成一个 ObjectWaiter 并加入 _WaitSet 列表中。当线程调用锁对象的 notify() 方法后,会根据情况将 _WaitSet 列表中的元素转移到 _cxq 列表或 _EntryList 列表,等到获取锁的线程释放锁后,再根据条件来执行该方法。

当遇到多个线程同步处理时,ObjectMonitor 状态变化如下:
为了便于大家理解上述过程,我们整理了相应的流程图,如下图所示。



图 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 和 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 标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

相关文章