synchronized之重量级锁的实现原理

首先,请不要因为标题就做以下的断言:synchronized锁是重量级锁。
这个断言是错误的!!!JDK1.6对synchronized做了优化,synchronized锁有一个升级的过程,升级到最后才会变成重量级锁!

本文我们讨论synchronized重量级锁的实现原理。

1 synchronized的实现原理

1.1 synchronized修饰符对应的字节码指令

我们都知道synchronized可以作用在方法上,也可以作用代码块内。

package synchronizedTest;

public class SynchronizedTest {
    // 作用于方法上
    public synchronized void test(){
        System.out.println("synchronized test!!!");
    }
    // 作用于代码块内
    public void testBlock(){
        synchronized (this) {
            System.out.println("synchronized test!!!");
        }
    }
}

通过javap -v SynchronizedTest.class反编译后
1、test()反编译后的代码如下图:


synchronized方法edit.jpg

2、testBlock()反编译后的代码如下图:


synchronized代码块.jpg

从上图可以看到:
1、synchronized方法反编译后,输出的字节码有ACC_SYNCHRONIZED标识
2、synchronized代码块反编译后,输出的字节码有monitorenter和monitorexit语句

由此我们可以猜测
1、synchronized的作用域不同,JVM底层实现原理也不同
2、synchronized代码块是通过monitorenter和monitorexit来实现其语义的
3、synchronized方法是通过ACC_SYNCRHONIZED来实现其语义的

那么虚拟机字节码引擎是怎么解读monitorenter/monitorexit和ACC_SYNCRHONIZED的?下一部分我们就介绍这个。

1.2 monitorenter/monitorexit实现原理

我们先看一下JVM规范是怎么定义monitorenter和monitorexit的👇👇👇
(1) monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

翻译过来:
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

(2) monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译过来:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

1.3 ACC_SYNCRHONIZED实现原理

当JVM执行引擎执行某一个方法时,其会从方法区中获取该方法的access_flags,检查其是否有ACC_SYNCRHONIZED标识符,若是有该标识符,则说明当前方法是同步方法,需要先获取当前对象的monitor,再来执行方法。

2 监视器monitor

由1.2和1.3的描述,可以看出无论是synchronized代码块还是synchronized方法区,其线程安全的语义实现最终依赖一个叫monitor的东西,那么这个神秘的东西是什么呢?下面让我们来详细介绍一下。

2.1 monitor的神秘面纱

每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

我们的java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。

在hotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于hotSpot虚拟机源码ObjectMonitor.hpp文件中。ObjectMonitor主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //monitor进入数
    _waiters      = 0,
    _recursions   = 0;  //线程的重入次数
    _object       = NULL;
    _owner        = NULL; //标识拥有该monitor的线程
    _WaitSet      = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁进入时的单项链表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

① owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
② _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
③ _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中
④ _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中

_cxq队列为什么是个单向链表,线程是被按照什么顺序放入_cxq队列中,_cxq队列中哪些线程是有资格成为候选资源的线程,
ObjectMonitor的数据结构中包含三种队列:_WaitSet、_cxq和_EntryList,它们之间的关系转换可以用下图表示:

我们可以猜测对象的monitor的生成时机,可能对象创建的同时,也可能时当执行到某个对象的synchronized方法,发现当前对象没有monitor对象,而为其创建一个monitor对象。

java对象对应的monitor也是一个临界资源,其线程安全由虚拟机自身代码来保证,开发者无需考虑。

推荐阅读更多精彩内容