实现重入锁ReentrantLock锁使用到的技术
- CAS 保证操作原子性
- AQS 带有头尾节点的队列 链表实现
Node {
//Node代表了等待的线程
Node prev 前一个Node
Node next 后面一个Node
}
AQS{
Node head 头节点
Node tail 尾结点
}
- 自旋操作
用到的一些方法的含义(公平锁实现情况)(代码为Open-JDK13中的)
- tryAcquire 本线程尝试获取锁,获取成功返回true
- hasQueuedPredecessors 判断本线程是否需要排队,需要排队返回true
- addWaiter 往队列中添加一个Node 即添加一个排队的线程
源码具体执行流程:
步骤一:
步骤二: 获取锁.png
步骤三:
获取锁具体过程.png
这里开始麻烦起来了,第一步本线程第一次尝试获取锁,调用 tryAcquire, 如果获取成功,则上锁过程结束,如果获取失败就执行另外两步。
先看下tryAcquire干什么了(为公平锁实现的tryAcquire)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
获取锁的状态
int c = getState();
如果状态为0,说明此时锁时空闲的
但是注意可能有多个线程同时检测到锁空闲,那么就会有多个线程都进入下面的IF
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
... 省略一部分代码,先不讨论
return false;
}
如果是空闲状态,那么就要先看看是不是要排队(因为可能有多个线程同时,非公平锁就不用检测要不要排队)
public final boolean hasQueuedPredecessors() {
Node h, s; h表示头结点,s表示后续节点
如果h==null 那么就说明此此时队列都没创建 本线程是第一个线程,说明没有其他线程,就**不用排队**
if ((h = head) != null) {
s==null说明下一个没有 但也可能因为并发的原因 不能保证后面也是空的
s.waitStatus>0 说明下一个被取消了,要看后面的是什么情况
if ((s = h.next) == null || s.waitStatus > 0) {
s = null; // traverse in case of concurrent cancellation
for (Node p = tail; p != h && p != null; p = p.prev) { 从后往前找
找到一个不是null的也不是被取消的
也就是找到队列中第一个真正在等待的
if (p.waitStatus <= 0)
s = p;
}
}
不用排队情况(人即线程)
情况一:根本没有在等待,前面可能有人,也可能没有人
情况二:有在等待的,但是等待的人是自己,前面肯定有人
要排队的情况
有在真正等待的,并且第一个真正在等待的不是自己
s==null 就说明没有等待的 **不用排队** 队列已经初始化过了
s!=null就说明有等待的,
等待的人不是自己 **要排队**
等待的人是自己 **不用排队** 这里会比较困惑,因为这个情形的出现要结合后面的**acquireQueued方法来看**
因为如果你是第一个等待者,那么acquireQueued会再调用一次tryAcquire,而tryAcquire会再调用一次hasQueuedPredecessors,
那么这种情形就会出现了即等待的人是自己
if (s != null && s.thread != Thread.currentThread())
return true;
}
return false;
}
这个判断排队比较绕。。。
然后就是回到了之前的tryAcquire了,如果不需要排队,那么直接用CAS操作尝试修改锁的状态,如果修改成功那么获取锁就成功了。
如果不需要带队,但是获取锁失败了那就说明实际上还是需要排队的,因为这种情况就说明了存在多线程竞争
如果需要排队就要调用addWaiter往队列中添加等待者,即排队,这个方法内部就不展开了,就是入队,它用CAS操作保证了入队顺序是线程安全的。 注意排好队后,线程还没有阻塞。
排好队后队列中排队的人看看自己能不能获取锁,如果不能就阻塞线程,直到被唤醒
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
死循环,直到获取锁为止,当然获取失败就会阻塞,直到被唤醒,然后再尝试获取 如此循环
for (;;) {
final Node p = node.predecessor();
情形一 加入队列后如果自己是第一个等待的那么就在试着获取一次锁,
原因是再自己第一次尝试获取锁失败加入队列后的这段时间,拥有锁的那个线程可能释放了锁
更后面的等待者是没有机会尝试获取锁的
情形二阻塞后被唤醒
说明自己是第一个等待的 再尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
决定是否阻塞 根据它前面的人的状态
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt(); 阻塞,并且阻塞醒来也是从这里开始
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
shouldParkAfterFailedAcquire这个方法也是比较令人困惑的地方,也是我自己理解还不到位的地方
这个方法其实会执行两遍,第一遍是把前面等待者(如果有的话)的等待状态设置好(设置成阻塞等待)
第二遍再确定自己是否需要阻塞
如果你都因为获取锁失败
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
前面一个等待者的等待状态
int ws = pred.waitStatus;
说明前面一个已经在阻塞等待了,那么自然我也就要阻塞等待了
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
你前面的等待者(等待线程)可能被取消了 找到一个没有被取消的
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
第一次执行 把前面的等待者状态设置好,因为它肯定已经阻塞等待
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
这里说的比较乱。。。
为什么要后面的人来设置前面的人的阻塞状态呢,因为一个线程真正阻塞了,那么它的阻塞状态才能设置,即先有阻塞,然后有阻塞状态,但是阻塞的线程就根本不能动了,自然也不能设置阻塞状态。
拿睡觉来打个比方,你睡着还是没睡着不能由你说了算,你不可能告诉别人你睡着了,只能由别人来看,只有别人才能看到你是否真正睡着了。
并且这段有解锁过程相关性挺高的,以后有时间再说了。
这篇文章主要是为了记录自己在学习AQS时候的一些感受,也方便以后自己复习。所以用到的语言也不够准确,过程不能保证正确。如果有同学看到这篇文章,一定要带着怀疑的心态去看。如果发现错误的地方希望能批评指出。
网友评论