重量级锁(Monitor)加锁流程和解锁流程
重量级锁 MonitorObject 对象有 4 个属性,分别是:
-
_owner:当前锁的持有线程
-
_cxq:竞争栈
-
_entryList:一个队列
-
_waitSet:
加锁流程:
当线程 t1、t2、t3 一起获取一个重量级锁时,获取的时间顺序分别是 t1、t2 和 t3。
1、因为是线程 t1 首先到达,所以 t1 会获取成功, MonitorObject 的 _owner 会从 nullptr 变成 t1,线程 t1 的 markword 对象存储 MonitorObject 的地址引用并将最后两位标记为 10,表示重量级锁。此时线程 t2 和 t3 肯定获取锁失败。
2、线程 t2,t3 开获取失败后悔开始进行自旋操作【jdk1.6 以后固定自旋就弃用了】,首先预自旋 11 次,获取锁失败之后会自适应自旋,首先自旋 5000 次。在自旋期间若线程 t1 释放锁了,此时线程 t2 和 t3 会一起去抢占锁,若没有释放就会进入 _cxq 竞争栈中。这段时间还是抢占式的。
3、若第二步获取锁失败了,就会进入一个叫做 enterI 的方法,尝试获取 _owner ,失败之后会陷入自旋,较上次自旋次数少 200 次,若自旋期间获取成功就成功拿到锁了,这段时间还是抢占式的。
4、若第 3 步中还是获取失败了那么线程就会在 _cxq 中陷入阻塞状态了(park),直到 _owner 被释放才会被唤醒。从这里开始就是非抢占式的了。靠后竞争锁的线程会优先获取到锁。
解锁流程:
当线程 t1 释放锁之后,就会将 _owner 设置成 nullptr。此时会根据 _cxq 和 _entryList 的状态做出不同的操作。
1、当_cxq 和 _entryList 都为空时直接返回,释放成功。
2、当 _cxq 不为空时,就会将 _cxq 中所有的节点移动到 _entryList 中,_cxq 按照后进先出的原则,之后进入 _cxq 的会先进入 _entryList。
3、当 _entryList 不为空时,使用 unpark 方法从队列头结点开始唤醒,然后返回。
所以说,根据上面的加锁流程,当 t1 释放锁之后,进入 _cxq 的顺序是先 t2 后 t3,所以离开 _cxq 进入 _entryList 的顺序是先 t3 后 t2。故在 t2 和 t3 中,t3 会先获得锁。
解锁是有序的验证
查看下列代码
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("t1 获取锁");
synchronized (obj) {
try {
System.in.read();
System.out.println("t1 释放");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(100);
new Thread(() ->{
synchronized (obj){
System.out.println("t2 获取");
}
}).start();
Thread.sleep(100);
new Thread(() ->{
synchronized (obj){
System.out.println("t3 获取");
}
}).start();
Thread.sleep(100);
new Thread(() ->{
synchronized (obj){
System.out.println("t4 获取");
}
}).start();
}
运行结果如下(运行多次结果都是一样的):从结果可以看出,当线程 t1 释放锁后,越靠后竞争锁的线程或优先抢占到锁。这就是上面加锁流程中第 4 步的体现。
t1 获取锁
t1 释放
t4 获取
t3 获取
t2 获取
Process finished with exit code 0