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虚拟机在调用方法时自动执行的监视器进入和退出 被视为发生。

    同步代码块指令 文档

     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相关联的监视器 ,则该线程将阻塞,直到该监视器的条目计数为零为止,然后再次尝试获取所有权。

    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关联的监视器的条目计数。结果,如果条目计数的值为零,则线程退出监视器,并且不再是其所有者。其他被阻止进入监视器的线程也可以尝试这样做。

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

      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 对象头

      对象头知识参考

|-------------------------------------------------------|--------------------|
|                  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需要重新竞争锁

     

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

    

  

原文地址:https://www.cnblogs.com/qingshan-tang/p/12698705.html

时间: 2024-11-14 14:11:43

java并发系列-monitor机制实现的相关文章

Java并发系列[5]----ReentrantLock源码分析

在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析

学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等.而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类,由此可见这个类的重要性.所以在Java并发系列文章中我首先对AbstractQueuedSynchronizer这个类进行分析,由于这个类比较重要,而且代码比较长,为了

Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态.理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解.在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作.AQS为在独占模

Java中的monitor机制

monitor概念 管程,监视器.在操作系统中,存在着semaphore和mutex,即信号量和互斥量,使用基本的mutex进行开发时,需要小心的使用mutex的down和up操作,否则容易引发死锁问题.为了更好的编写并发程序,在mutex和semaphore基础上,提出了更高层次的同步原语,实际上,monitor属于编程语言的范畴,C语言不支持monitor,而java支持monitor机制. 一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monito

【Java并发系列04】线程锁synchronized和Lock和volatile和Condition

img { border: solid 1px } 一.前言 多线程怎么防止竞争资源,即防止对同一资源进行并发操作,那就是使用加锁机制.这是Java并发编程中必须要理解的一个知识点.其实使用起来还是比较简单,但是一定要理解. 有几个概念一定要牢记: 加锁必须要有锁 执行完后必须要释放锁 同一时间.同一个锁,只能有一个线程执行 二.synchronized synchronized的特点是自动释放锁,作用在方法时自动获取锁,任意对象都可做为锁,它是最常用的加锁机制,锁定几行代码,如下: //---

java并发系列(六)-----Java并发:volatile关键字解析

在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题.一旦一个共享变量被 volatile关键字 修饰,那么就具备了两层语义:内存可见性和禁止进行指令重排序.在多线程环境下,volatile关键字 主要用于及时感知共享变量的修改,并使得其他线程可以立即得到变量的最新值,例如,用于 修饰状态标记量 和 D

java并发系列 - 第29天:高并发中常见的限流方式

这是java高并发系列第29篇. 环境:jdk1.8. 本文内容 介绍常见的限流算法 通过控制最大并发数来进行限流 通过漏桶算法来进行限流 通过令牌桶算法来进行限流 限流工具类RateLimiter 常见的限流的场景 秒杀活动,数量有限,访问量巨大,为了防止系统宕机,需要做限流处理 国庆期间,一般的旅游景点人口太多,采用排队方式做限流处理 医院看病通过发放排队号的方式来做限流处理. 常见的限流算法 通过控制最大并发数来进行限流 使用漏桶算法来进行限流 使用令牌桶算法来进行限流 通过控制最大并发数

【Java并发系列01】Thread及ThreadGroup杂谈

img { border: solid black 1px } 一.前言 最近开始学习Java并发编程,把学习过程记录下.估计不是那么系统,主要应该是Java API的介绍(不涉及最基础的概念介绍),想要深入系统学习推荐看一本书<Java Concurrency in Practice >(建议看英文,也可以看中文译本:< Java 并发编程实战>). 并发编程的基础就是线程,所以这一篇对线程做初步了解. 二.Thread和ThredGroup的关系 因为Thread的构造函数中有

java并发系列(一)-----多线程简介、创建以及生命周期

进程.线程与任务进程:程序的运行实例.打开电脑的任务管理器,如下: 正在运行的360浏览器就是一个进程.运行一个java程序的实质是启动一个java虚拟机进程,也就是说一个运行的java程序就是一个java虚拟机进程.进程是程序向操作系统申请资源(如内存空间和文件句柄)的基本单位. 线程:是进程中可独立执行的最小单位,并且不拥有资源.进程相当于工厂老板,整个工厂的机器都是属于老板的,但是工厂里面的活都是由工人完成的. 任务:线程所要完成的计算就被称为任务,特定的线程总是执行特定的任务. java