0 什么是Monitor?
Monitor是一个同步工具,或者说是一种同步机制;
Monitor的特点:
- 互斥
- 提供singnal机制(解决线程间的通信)
可以看到,Monitor的特点刚好对应于锁所要解决的两个问题。事实上Monitor就是一种锁的实现方式。 Monitor的底层实现依赖于操作系统的Mutex Lock。
java中的Monitor:java中的每个对象会关联一个Monitor对象,这个Monitor对象实现了Monitor机制,通常作为管程来控制同步代码的实现。Monitor是JVM内置的重量级锁机制(synchronized重量级锁实现)。
1 Moniotr在JVM中的实现
在JVM中Monitor是线程私有的数据结构,每个线程都有一个Monitor列表,同时还有一个全局的Monitor列表;同时在java中一切皆对象,Monitor也不列外,其被封装成为了Monitor对象(ObjectMonitor)与对应的普通对象对象头中的MarkWord相关联;
ObjectMonitor底层由C++实现,在OpenJDK对应的目录可以查看到其源码(地址:jdk8:runtime)
ObjectMonitor类有几个关键的属性:
- _owner:指向持有ObjectMonitor对象的线程,同一时刻只能指向一个线程
- _cxq: 多个线程争抢锁,会先存入这个单向链表
- _EntryList:存放处于等待锁block状态的线程队列
- _WaitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
- _recursions: 记录重入次数
- _count:约为 _WaitSet 和 _EntryList 的节点数之和
Monitor机制的具体实现
Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。
当一个线程需要访问同步代码块的时候,会先通过锁定对象LockObject的对象头中的Markword找到对应的Monitor对象,然后需要获取Monitor对象的所有权;它会首先在 _EntryList 入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的 Monitor,那么它会和 _EntryList 队列中的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的 Monitor,执行同步代码块,执行完毕后释放Monitor;如果已经有线程持有对象的 Monitor,那么需要等待其释放 Monitor 后再进行竞争。
当一个线程拥有Monitor后,如果调用Object(指的是LockObkect)的 wait()
方法,线程就释放了Monitor,进入 _WaitSet 队列,等待Object的 notify()
方法。当该对象调用了 notify()
方法或者 notifyAll()
方法后, _WaitSet 中的线程就会被唤醒,放进 _EntryList 队列进行新一轮竞争。
以上只是简单示意。事实上,锁的竞争要比上面描述的更为复杂,除了_EntryList 这个双向链表用来保存竞争的线程,ObjectMonitor中还有另外一个单向链表 _cxq,由两个队列来共同管理并发的线程。
源码详细实现
查看源码可以知道,ObjectMonitor::enter() 和 ObjectMonitor::exit() 分别是ObjectMonitor获取锁和释放锁的方法。整个monitor机制就是操控 _cxq, _EntryList, _Waitet这三大队列实现的。
- _cxq队列是一个以 _cxq为头节点的队列,单向链表;当一个线程尝试 CAS 获取monitor锁失败后,最终会被封装成一个 ObjectWaiter 对象,并放入 _cxq队列中的头节点的下一位,即会插入到 _cxq 节点的后边(这里会并发竞争,是通过CAS实现指针更改的);因此synchronized是非公平锁,后来的节点反而被插入到头部; 从 Cxq 取得元素时,则会从队尾获取(只有 Owner 线程才能从队尾取元素,也即线程出列操作无争用);
- _EntryList 与 Cxq 在逻辑上都属于等待队列。Cxq 会被线程并发访问,为了降低对 Cxq 队尾的 争用,而建立 EntryList(正是因为EntryList的存在,cxq才没有出队并发竞争)。在 Owner 线程释放锁时,JVM 会从 Cxq 中迁移线程到 EntryList,并会 指定 EntryList 中的某个线程(一般为 Head)为 OnDeck Thread(Ready Thread)。EntryList 中的线程,作为候选竞争线程而存在。
- WaitSet是一个环形双向队列,Owner 线程被 Object.wait()方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 Object.notify()或者 Object.notifyAll()唤醒,在线程会重新进去 EntryList或 cxq 中。通常只有monitor owner能访问(wait进队,notify出队)。
- notify的作用:notify()方法其实就是移动waitset中的线程要么到cxq要么到entrylist中,当同步方法结束的时候会触发唤醒机制,根据Qmode不同类型进行不同的规则唤醒。因此notify方法其实并不负责唤醒线程;notify会根据Policy 的不同作出的操作:
- Policy== 0 :放入到entrylist队列的排头位置
- Policy== 1 :放入到entrylist队列的末尾位置
- Policy== 2 :判断entrylist是否为空,为空就放入entrylist中,否则放入cxq队列排头位置(默认 策略)
- Policy==3 :判断cxq是否为空,如果为空,直接放入头部,否则放入cxq队列末尾位置
对于enter():竞争锁
对于exit():唤醒等待竞争的线程,每次同步线程退出时自动唤醒
- 根据QMode策略进行唤醒:
- QMode=2,取cxq头部节点直接唤醒
- QMode=3,如果cxq非空,把cxq队列放置到entrylist的尾部(顺序跟cxq一致)
- QMode=4,如果cxq非空,把cxq队列放置到entrylist的头部(顺序跟cxq相反)
- QMode=0,啥都不做,继续往下走(QMode默认是0)默认是0 Qmode=0的判断逻辑就是先判断entrylist是否为空,如果不为空,则取出第一个唤醒,如 果为空再从cxq里面获取第一个唤醒
2 验证notify()的实际作用
用两个线程验证一下wait()
、notify()
对线程状态的影响;
值得注意的是notify()
只会转移线程到Cxq或者EntryList中,并不会立即唤醒执行;只有当前线程执行完毕,退出执行exit()
的时候才会唤醒EntryList的首节点线程(如果EntryList不为空的话,默认);
进入WaitSet()的线程,状态为:WAITING;进入cxq/entryList的线程,状态为:BLOCKED;进入sleep()的线程,状态为:TIMED_WAITING;
详细可见代码及注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Main {
public static void main(String[] args) throws InterruptedException {
byte[] lock = new byte[0];
Runnable task1 = () -> { //开启后休眠3s并调用wait() 然后打印 finish
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ": begin");
try {
Thread.sleep(3000);
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": finish");
}
};
Runnable task2 = () -> {//开启后休眠3s 调用notify() 然后 打印 finish
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ": begin");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.notify();
System.out.println(Thread.currentThread().getName() + ": finish");
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start(); //休眠3s后进入WaitSet 并释放掉monitor (此时没有打印finish)
Thread.sleep(1500);
t2.start(); //此时t1持有monitor在睡眠,因此t2会进入到_cxq队列的队首
System.out.println("t1: " + t1.getState()); //此时t1处于 TIMED_WAITING 状态(进入了sleep())
System.out.println("t2: " + t2.getState()); //此时t2处于 BLOCKED状态(进入了cxq队列)
Thread.sleep(2000); //3s结束 t1调用wait() 释放了锁 进入WaitSet队列
System.out.println("t1: " + t1.getState()); //此时t1处于 Waiting状态(进入了WaitSet队列)
//t2调用的notify会把t1从WaitSet中转移到entrylist中(默认 策略2),
//然后t2会执行完毕打印finish,退出释放锁的时候执行exit()唤醒entrylist的第一个线程t1继续执行打印finish
//因此最终t2一定比t1先执行完毕;
// 执行结果:
// t1: begin
// t1: TIMED_WAITING
// t2: BLOCKED
// t2: begin
// t1: WAITING
// t2: finish
// t1: finish
}
}
3 总结
Monitor机制是java内置锁的实现方式。具体是通过三个队列Cxq+EntryList+WaitSet结合CAS机制来实现的;
线程在三个队列中的转移是通过wait()
和notify()
方法来实现的;大致转移流向图如下图所示(线程在队列中的实际顺序可能根据采用的唤醒模式及notify()策略有关):