前言
咱们下面都是公平锁相关的内容
参考视频:https://www.bilibili.com/video/BV1XJ411N7n8?p=1
基本原理
首先大概要知道总体原理:
多个线程去抢一个status状态,抢到的线程就获取成功,抢不到线程的会把自己当前线程放在一个队列里排队。抢到锁的线程unlock的时候会把排在队头的线程唤醒,然后队头的线程再去尝试获得锁。
大概的伪代码是这样的,混个脸熟
class Lock {
int state;
Queue q;
lock() {
for(;;) {
// 多线程抢着设置成1
if (CAS(state, 0, 1)) {
return true;
} else {
// 抢不到的线程入队并且park等待唤醒
enq(currentThread);
park();
}
}
}
unlock() {
state = 0;
// 取出队头线程
Thread t = getq();
// 唤醒他
t.unpark();
}
}
模拟多线程加锁解锁过程
看并发的代码太蛋疼了,特用下面的表格模拟了一下多个线程交替运行的过程,这样能比干看代码相对好理解一点吧。
把你的大脑当做计算机,一行一行的运行代码吧!
备注 | t1 | t2 | t3 | 重要变量 |
---|---|---|---|---|
| acquire(1); |
|
|
|
| tryAcquire(1) |
|
|
|
| 获取当前thread=t1 |
|
| |
| 初始化查state==0 |
|
| state=0 |
| hasQueuedPredecessors返回false,不需要排队 //队列为空:head==tail, |
|
|
|
| CAS设置state=1 设置当前获得锁的线程=t1 |
|
| state=1 |
| tryAcquire(1)返回true成功获得锁,返回 |
|
|
|
跟t1的前面流程是一样的流程 |
| acquire(1); tryAcquire(1) 查state==1, tryAcquire(1)返回false |
| state=1 |
|
| acquireQueued (addWaiter(Node.EXCLUSIVE), arg))
// addWaiter在这时候会创建一个双向链表, 有两个结点, 头结点的thread=null, 尾结点的thread=t2 |
| state=1 链表: 空头结点-t2 |
万一这时候t1已经跑完了 ,t2再尝试获取锁就可以获取锁了, 不用park,减小性能消耗。 咱们就假设真的发生了这个神奇的事件, 继续推演 |
| 进入acquireQueued。 死循环, 获取t2的前一个节点, 发现是那个thread为空的头结点, 于是再尝试获取锁tryAcquire(arg) |
|
|
t1还真的就释放锁了 (过程先不分析,但是大胆认为state==0) | state==0 | hasQueuedPredecessors() // 这时候t2之前建好了双向链表, h!=t,(s=h.next)就是t2, 最后整体判断 hasQueuedPredecessors() 返回false 认为没人排队 |
| state=0 链表: 空头结点-t2 |
|
| CAS设置state=1 设置当前获得锁的线程=t2返回true |
| state=1 链表: 空头结点-t2 |
注意! 到这里我们大概可以猜测, 这个队列维护的时候, 一直是有个空节点当头结点, 排在他后面的才是实际上排第一的节点。 |
| setHead(node);设置头是当前节点(会把当前节点的thread设置为null,这样就变成了跟原来造的空头结点一模一样了),头节点设置为null(help GC)返回interrupted=false; |
| state=1 链表: 空头结点 |
两个条件都返回false,不会调用selfInterrupt()
t2成功获取锁 |
| public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } |
|
|
t3来了, 这回可没有t2运气这么好了, 第2次tryAcquire也失败 |
|
| t3也执行lock,也跟前面一大坨,来到acquireQueued,第2次尝试tryAcquire也失败 | state=1 链表: 空头结点-t3 |
|
|
| shouldParkAfterFailedAcquire(p, node) // ws初始化都是0,进入最后一个else分支,CAS设置前面一个节点(头结点)的waitStatus=Node.SIGNAL=-1; | state=1 链表: 空头结点(ws=-1)-t3 |
|
|
| 注意现在还是在for(;;)死循环里。又会tryAcquire(arg),很不幸还是没有成功,又进入了shouldParkAfterFailedAcquire(p, node),这回pred前面那个空节点的ws==Node.SIGNAL了,返回true。 |
|
java.util.concurrent.locks. AbstractQueuedLongSynchronizer #parkAndCheckInterrupt |
|
| private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } //终于可以park了,乖乖等着被人唤醒吧 |
|
那我们就假设t2忙完了要unlock了吧 |
| release(1) tryRelease(1) int c = getState() - releases; //正常是0 setExclusiveOwnerThread(null); //设置当前持有锁的线程是null setState(c); //state被修改成了0,印证了我们前面的解锁会设置state=0的说法 返回true |
| state=0 链表: 空头结点(ws=-1)-t3 |
java.util.concurrent.locks. AbstractQueuedSynchronizer#release |
| 现在头结点不为空,头结点的ws被t3设置为了SIGNAL,所以要unparkSuccessor(Node node)
|
|
|
java.util.concurrent.locks. AbstractQueuedSynchronizer #unparkSuccessor |
| 现在ws=-1 CAS设置state为0 获取s为头结点后面那个节点,就是排第一的节点,也就是t3的节点、 s!=null, s.waitStatus == 0 不进入if分支(这个if分支,按照注释,是用来处理一些cancel的节点,先不管) 于是就unpark了t3 |
| state=0 链表: 空头结点(ws=-1)-t3 |
t3被唤醒了哟,回到了代码park的代码中
|
|
| return Thread.interrupted(); //由于是唤醒,不是被中断,返回false。 这时候都忘了是从哪个函数进的了,翻翻上面,其实是 java.util.concurrent.locks.AbstractQueuedLongSynchronizer#acquireQueued 返回false,不会进入if 分支。 又继续死循环 |
|
现在链表的状态是头部空节点后面连着t3 |
|
| tryAcquire(arg) state被t2设置为0 了 hasQueuedPredecessors返回false CAS state=1 设置当前持有锁的线程是t3,返回true | state=1 链表: 空头结点(ws=-1)-t3(ws=0) |
java.util.concurrent.locks. AbstractQueuedSynchronizer #acquireQueued |
|
| 设置头是t3,头的thread是null。这时候队列里只有一个空的头节点了 返回interrupted=false 成功获得了锁 | state=1 链表: 空头结点(ws=0) //因为把t3设置成了空的头结点所以ws是t3的ws=0 |
然后t3干完活再释放锁 |
|
| 偷懒抄下t2的释放锁 release(1) tryRelease(1) int c = getState() - releases; //正常是0 setExclusiveOwnerThread(null); //设置当前持有锁的线程是null setState(c); 返回true | state=0 链表: 空头结点(ws=0)
|
留一个坑:
什么时候触发hasQueuedPredecessors的其他条件?
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
以下这些情况都返回true:认为有人排队
头不等于尾 且 排第1的是空?? 这个是遗留问题
头不等于尾 且 排第一的不是当前线程 这个已经得到验证
结语
人脑代替电脑分析过程中发现了 interrupt 这种中断机制
后面有机会再分析是怎么个中断法。