背景
在 jdk1.6 以前 synchronized 的 java 内置锁不存在 偏向锁 ->轻量级锁 ->重量级锁 的锁膨胀机制, 锁膨胀机制是 1.6 之后为了优化 java 线程同步性能而实现的. 而 1.6 之前都是基于 monitor 机制的重量级锁. 因为 java 内部对锁实现的封装, 就算现在我们也只需要了解重量级锁就可以了. 深入了解 monitor 机制对学习线程同步非常重要.
正文
目录
什么是 monitor
monitor 的作用
monitor 的组成
寻找 monitor 锁
java monitor 机制的实现
什么是 monitor 参考 https://www.jianshu.com/p/7f8a873d479c
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 中的每个对象都可以作为锁.
普通同步方法, 锁是当前实例对象.
静态同步方法, 锁是当前类的 class 对象.
同步代码块, 锁是括号中的对象.
上面的官方文档也说了每个对象都与一个监视器关联. 有理由猜测, 任意的 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 锁神秘面纱
详情参考 https://www.cnblogs.com/webor2006/p/11442551.html
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 线程的千里追踪
参考 https://www.cnblogs.com/longshiyVip/p/5213771.html
线程访问同步代码, 需要获取 monitor 锁
线程被 jvm 托管
jvm 获取充当临界区锁的 java 对象
根据 java 对象对象头中的重量级锁 ptr_to_heavyweight_monitor 指针找到 objectMonitor
将当前线程包装成一个 ObjectWaiter 对象
将 ObjectWaiter 假如_cxq(ContentionList)队列头部
_count++
如果 owner 是其他线程说明当前 monitor 被占据, 则当前线程阻塞. 如果没有被其他线程占据, 则将 owner 设置为当前线程, 将线程从等待队列中删除, count--.
当前线程获取 monitor 锁, 如果条件变量不满足, 则将线程放入 WaitSet 中. 当条件满足之后被唤醒, 把线程从 WaitSet 转移到 EntrySet 中.
当前线程临界区执行完毕
Owner 线程会在 unlock 时, 将 ContentionList 中的部分线程迁移到 EntryList 中, 并指定 EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程).Owner 线程并不直接把锁传递给 OnDeck 线程, 而是把锁竞争的权利交个 OnDeck,OnDeck 需要重新竞争锁
大概流程就是这样的, 但是其中还有很多没有在这篇博客中提及的知识点就不深入了.
来源: http://www.bubuko.com/infodetail-3508166.html