java并发系列-monitor机制实现

Posted 青衫执卷

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发系列-monitor机制实现相关的知识,希望对你有一定的参考价值。

 背景

  在jdk1.6以前synchronized的java内置锁不存在 偏向锁->轻量级锁->重量级锁 的锁膨胀机制,锁膨胀机制是1.6之后为了优化java线程同步性能而实现的。而1.6之前都是基于monitor机制的重量级锁。因为java内部对锁实现的封装,就算现在我们也只需要了解重量级锁就可以了。深入了解monitor机制对学习线程同步非常重要。

 

正文

  目录

  1. 什么是monitor
  2. monitor的作用
  3. monitor的组成
  4. 寻找monitor锁
  5. java monitor机制的实现

 

什么是monitor 参考

  monitor直译过来是监视器的意思,专业一点叫管程。monitor是属于编程语言级别的,它的出现是为了解决操作系统级别关于线程同步原语的使用复杂性,类似于语法糖,对复杂操作进行封装。而java则基于monitor机制实现了它自己的线程同步机制,就是synchronized内置锁。

 

monitor的作用

  monitor的作用就是限制同一时刻,只有一个线程能进入monitor框定的临界区,达到线程互斥,保护临界区中临界资源的安全,这称为线程同步使得程序线程安全。同时作为同步工具,它也提供了管理进程,线程状态的机制,比如monitor能管理因为线程竞争未能第一时间进入临界区的其他线程,并提供适时唤醒的功能。

 

monitor的组成

  3.1 monitor对象

    monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程互斥就是通过mutex互斥锁实现的。

  3.2 临界区

    临界区是被synchronized包裹的代码块,可能是个代码块,也可能是个方法。

  3.3 条件变量

    条件变量和下方wait signal方法的使用有密切关系 。在获取锁进入临界区之后,如果发现条件变量不满足使用wait方法使线程阻塞,条件变量满足后signal唤醒被阻塞线程。 tips:当线程被signal唤醒之后,不是从wait那继续执行的,而是重新while循环一次判断条件是否成立。参考

  3.4 定义在monitor对象上的wait() signal() signalAll()操作

 

java中monitor的实现

  4.1 首先先看一下synchronized同步代码块和同步方法编译后的字节码指令文件分别是什么样子

    源代码如下 

public class SynchronizedTest {
    public synchronized void test1(){
    }
    public void test2(){
        synchronized (this){
        }
    }
}

 

接着我们用javap查看

  

 

   

 

   

  从上面可以看出,同步方法jvm是使用ACC_SYNCHRONIZED方法访问标识符实现同步,同步代码块jvm是使用monitorenter和monitorexit指令包裹临界区实现同步。

  

  4.2 线程执行到同步方法处和同步代码块monitorenter和monitorexit指令分别发生了什么

    这里需要看jvm的官方文档,下面三段话要好好读一读,monitor的运行逻辑都包含在里面。

    同步方法 文档

2.11.10. Synchronization
The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.

Method-level synchronization is performed implicitly, as part of method invocation and return (§2.11.8). A synchronized method is distinguished in the run-time constant pool\'s method_info structure (§4.6) by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

Synchronization of sequences of instructions is typically used to encode the synchronized block of the Java programming language. The Java Virtual Machine supplies the monitorenter and monitorexit instructions to support such language constructs. Proper implementation of synchronized blocks requires cooperation from a compiler targeting the Java Virtual Machine (§3.14).

Structured locking is the situation when, during a method invocation, every exit on a given monitor matches a preceding entry on that monitor. Since there is no assurance that all code submitted to the Java Virtual Machine will perform structured locking, implementations of the Java Virtual Machine are permitted but not required to enforce both of the following two rules guaranteeing structured locking. Let T be a thread and M be a monitor. Then:

The number of monitor entries performed by T on M during a method invocation must equal the number of monitor exits performed by T on M during the method invocation whether the method invocation completes normally or abruptly.

At no point during a method invocation may the number of monitor exits performed by T on M since the method invocation exceed the number of monitor entries performed by T on M since the method invocation.

Note that the monitor entry and exit automatically performed by the Java Virtual Machine when invoking a synchronized method are considered to occur during the calling method\'s invocation.


2.11.10。同步化
Java虚拟机通过单个同步结构(监视器)支持方法和方法中指令序列的同步。

作为方法调用和返回的一部分,方法级同步是隐式执行的(第2.11.8节)。甲synchronized方法是在运行时间常量池中的区分method_info结构(§4.6由)ACC_SYNCHRONIZED标志,这是由方法调用指令进行检查。调用方法时ACC_SYNCHRONIZED设置为1时,无论方法调用是正常完成还是突然完成,执行线程都将进入监视器,调用方法本身并退出监视器。在执行线程拥有监视器的时间内,没有其他线程可以进入它。如果在调用synchronized方法期间引发了异常并且该synchronized方法不处理该异常,则在将该异常重新抛出该方法之前,该方法的监视器将自动退出synchronized。

指令序列的同步通常用于对synchronizedJava编程语言的块进行编码 。Java虚拟机提供了 monitorenter和monitorexit指令来支持这种语言构造。正确实现synchronized块需要目标Java虚拟机(第3.14节)的编译器的配合。

当方法调用期间,给定监视器上的每个出口与该监视器上的先前条目匹配时,就是结构锁定。由于不能保证提交给Java虚拟机的所有代码都将执行结构化锁定,因此允许但不要求强制执行以下两个保证结构化锁定的规则的Java虚拟机实现。设 T为线程, M为监视器。然后:

进行监控条目的数量由Ť上中号的方法调用期间必须等于由执行监控退出的数目Ť上中号 是否该方法调用完成正常或突然的方法调用期间。

在一个方法调用期间没有点可以通过执行监控退出的数目Ť 上中号,因为该方法的调用超过执行监视器条目的数量Ť 上中号,因为该方法调用。

请注意,在调用synchronized方法时,Java虚拟机在调用方法时自动执行的监视器进入和退出 被视为发生。
View Code

 

 

    同步代码块指令 文档

     monitorenter

The objectref must be of type reference.

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.

该objectref的类型必须是reference。

每个对象都与一个监视器关联。监视器只有在拥有所有者的情况下才被锁定。执行monitorenter的线程 尝试获得与objectref关联的监视器的所有权,如下所示:

如果与objectref关联的监视器的条目计数 为零,则线程进入监视器,并将其条目计数设置为1。然后,该线程是监视器的所有者。

如果线程已经拥有与objectref关联的监视器 ,则它将重新进入监视器,从而增加其条目计数。

如果另一个线程已经拥有与objectref相关联的监视器 ,则该线程将阻塞,直到该监视器的条目计数为零为止,然后再次尝试获取所有权。
View Code

    monitorexit

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.

该线程减少与objectref关联的监视器的条目计数。结果,如果条目计数的值为零,则线程退出监视器,并且不再是其所有者。其他被阻止进入监视器的线程也可以尝试这样做。
View Code

    对比官方文档描述的同步方法和同步代码块指令,其实功能类似。总结如下

      1.同步方法和同步代码块都是通过monitor锁实现的。

      2.两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit指令来实现

      3.每个java对象都会与一个monitor相关联,可以由线程获取和释放。

      4.如果线程没有获取到monitor会被阻塞。

      5.monitor通过维护一个计数器来记录锁的获取,重入,释放情况。

 

    由此可知当线程执行到同步方法发现此方法有ACC_SYNCHRONIZED标志或者执行到monitorenter指令时,会去尝试获取monitor锁。

    那么就会有个疑问,既然线程需要获取monitor锁,那么什么是monitor锁,并且怎么才算获取monitor锁

  

  4.3 寻找monitor锁

    这里先不甩结论,接下来我们一步一步搜寻monitor锁。

    之前使用synchronized的时候知道,java中的每个对象都可以作为锁。

    1. 普通同步方法,锁是当前实例对象。
    2. 静态同步方法,锁是当前类的class对象。
    3. 同步代码块,锁是括号中的对象。

    上面的官方文档也说了每个对象都与一个监视器关联。有理由猜测,任意的java对象在实例化的时候都同时生成了一个monitor锁与之一一对应。那么进一步猜测,通过java对象可以获取到和它对应的监视器。

    这时候涉及到对象头的知识点。

    4.3.1 对象头

      对象头知识参考1 参考2

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

       每个java对象在内存中由对象头,实例数据和对齐填充三块区域组成。其中对象头存储了一些增强对象功能的信息,对象头中的Mark word 记录了锁的相关信息。如果此刻该对象锁升级为重量级锁,那么其中在对象头中存储了指向基于monitor锁的指针ptr_to_heavyweight_monitor。这个指针指向的就是我们苦苦寻找的锁。

      既然监视器是指针指向的内存区域,那么这块内存区域肯定有自己的数据结构,而这个数据结构保存着线程同步的所有信息。

 

   4.3.2 揭开monitor锁神秘面纱

         详情参考

         monitor的定义和初始化是有c语言编写的。

         http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/095e60e7fc8c/src/share/vm/runtime/objectMonitor.hpp 

 

 

 

     最重要的就是这两个c语言定义的类,objectMonitor就是对象头中指向的monitor重量级锁,objectWaiter是对等待线程的封装,可以用双向链表保存起来。

    下面解释objectMonitor中属性的含义

      

_header

定义:

volatile markOop   _header;       // displaced object header word - mark

说明:

  _header是一个markOop类型,markOop就是对象头中的Mark Word

 _count  定义:
volatile intptr_t  _count;        // reference count to prevent reclaimation/deflation
                                    // at stop-the-world time.  See deflate_idle_monitors().
                                    // _count is approximately |_WaitSet| + |_EntryList|
说明:抢占该锁的线程数 约等于 WaitSet.size + EntryList.size
 _waiters

 定义:

volatile intptr_t  _waiters;      // number of waiting threads

说明:等待线程数

_recursions   

定义:

volatile intptr_t  _recursions;   // recursion count, 0 for first entry

说明:锁重入次数

 _object  

定义:

void*     volatile _object;       // backward object pointer - strong root

说明:监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中

_owner 

定义:

void *  volatile _owner;          // pointer to owning thread OR BasicLock

说明:

指向获得ObjectMonitor对象的线程或基础锁

 _WaitSet

定义:

ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor

说明:处于wait状态的线程,被加入到这个linkedList

 _WaitSetLock

定义:

volatile int _WaitSetLock;        // protects Wait Queue - simple spinlock

说明:protects Wait Queue - simple spinlock ,保护WaitSet的一个自旋锁(monitor大锁里面的一个小锁,这个小锁用来保护_WaitSet更改)

_Responsible 

定义:

Thread * volatile _Responsible

说明:未知 参考:https://www.jianshu.com/p/09de11d71ef8

 _succ

定义:

  Thread * volatile _succ ;          // Heir presumptive thread - used for futile wakeup throttling

说明:当锁被前一个线程释放,会指定一个假定继承者线程,但是它不一定最终获得锁。参考:https://www.jianshu.com/p/09de11d71ef8

 _cxq

定义:

  ObjectWaiter * volatile _cxq ;    // LL of recently-arrived threads blocked on entry.
                                    // The list is actually composed of WaitNodes, acting
                                    // as proxies for Threads.

说明:ContentionList 参考:https://www.jianshu.com/p/09de11d71ef8

 FreeNext  

定义:

ObjectMonitor * FreeNext ;        // Free list linkage

说明:未知

_EntryList   

定义:

ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry.

说明:未获取锁被阻塞或者被wait的线程重新进入被放入entryList中

 _SpinFreq  

定义:

volatile int _SpinFreq ;          // Spin 1-out-of-N attempts: success rate

说明:未知 可能是获取锁的成功率

 _SpinClock

定义:

volatile int _SpinClock ;

说明:未知

 OwnerIsThread

定义:

int OwnerIsThread ;               // _owner is (Thread *) vs SP/BasicLock

说明:当前owner是thread还是BasicLock

 _previous_owner_tid

定义:

volatile jlong _previous_owner_tid; // thread id of the previous owner of the monitor

说明:当前owner的线程id

       其实上面的属性中我们真正需要了解的就几个。下面大概描述一下。

      4.3.3 线程的千里追踪

    

        参考

 

 

    1. 线程访问同步代码,需要获取monitor锁
    2. 线程被jvm托管
    3. jvm获取充当临界区锁的java对象
    4. 根据java对象对象头中的重量级锁 ptr_to_heavyweight_monitor指针找到objectMonitor
    5. 将当前线程包装成一个ObjectWaiter对象
    6. 将ObjectWaiter假如_cxq(ContentionList)队列头部
    7. _count++

    8. 如果owner是其他线程说明当前monitor被占据,则当前线程阻塞。如果没有被其他线程占据,则将owner设置为当前线程,将线程从等待队列中删除,count--。
    9. 当前线程获取monitor锁,如果条件变量不满足,则将线程放入WaitSet中。当条件满足之后被唤醒,把线程从WaitSet转移到EntrySet中。
    10. 当前线程临界区执行完毕
    11. Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交个OnDeck,OnDeck需要重新竞争锁

     

      大概流程就是这样的,但是其中还有很多没有在这篇博客中提及的知识点就不深入了。

    

  

以上是关于java并发系列-monitor机制实现的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程专题系列之深入分析synchronized(基础篇)

Java并发编程系列- 原子操作与CAS

java的monitor机制中,为啥阻塞队列用list等待队列用set

开发宝典Java并发系列教程

Java并发系列ReentrantLock源码分析

synchronized原理是啥?