Java 并发(十):独占式超时获取同步状态 (1)
private boolean doAcquireNanos(long arg, long nanosTimeout)
throws InterruptedException {
//判断设置的超时时间是否正确
//如果不正确,直接返
回 false
//那么上一层的 tryAcquirenano 方法整体就会返回 false
//那么就不可以继续执行下去了
if (nanosTimeout <= 0L)
return false;
//计算获取锁的限制时间
//超过这个时间就不可以获取了
//System.nanoTime 是一个本地方法,用来获取虚拟机时间的,精确到纳秒级别
//所以传进来的 nanosTimeout 必须为纳秒级别
// 1 秒 = 10^9 纳秒,十亿份之一
//这也是为什么使用 long 类型
final long deadline = System.nanoTime() + nanosTimeout;
//因为没抢到锁,所以要将该线程加入到队列里面
//并且 nextWaiter 为 Exclusive,代表该结点线程状态是独占式的
final Node node = addWaiter(Node.EXCLUSIVE);
//failed 变量记录过程是否出错
boolean failed = true;
try {
//死循环
for (;;) {
//下面这段代码就是自旋获取锁的逻辑
//获取上一个线程
final Node p = node.predecessor();
//从下面这个判断可以判断出,这个抢锁过程是公平的
//如果上一个线程是头结点
//并且自己尝试获取锁成功
if (p == head && tryAcquire(arg)) {
//那么该线程就可以继续运行了
//先将头结点设为自己
setHead(node);
//断开与头结点的连接
//让头结点可以回收
p.next = null; // help GC
//failed 为 false 代表自旋过程无出错
failed = false;
//自旋结束,返回 true,回到上一层继续运行
return true;
}
//上一个不是头结点,或者虽然是头结点但抢不到锁
//计算还需要等待多长时间
nanosTimeout = deadline - System.nanoTime();
//如果没有时间剩余了
if (nanosTimeout <= 0L)
//返回 false
//上一层处理
return false;
//还有时间剩余,看需不需要 park
//这里一样是自旋两次,然后就会返回 true,代表需要 Park
//继续判断剩余时间是否足够进行 park
//因为如果 park 需要的时间都要多于线程剩下的时间
//没什么必要 park 了,让其再自旋多一次,拿不到就超时返回 false
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//如果超时了,而且需要 park,那就 park 掉
//这里 park 掉,线程也是会继续进行倒计时的
//在这段时间内都会被 park 掉,这段时间内,只有 unpark 和 interrupt 可以恢复
//超过这个时间也会恢复,然后又一次自旋,然后超时 GG
LockSupport.parkNanos(this, nanosTimeout);
//判断唤醒该线程的方式是不是被别的线程 interrupt
//因为 park 是不会覆盖 interrupt 标志位的
if (Thread.interrupted())
//如果是被中止
//抛出异常
throw new InterruptedException();
}
} finally {
//同理,没有修改过 failed 变量,抛出异常,超时
//就会执行下面 cancelAcquire
if (failed)
cancelAcquire(node);
}
}
总结一下整个流程
判断等待时间是否合理
小于等于 0,不合理,直接返回 false
计算最大等待时间点,使用当前虚拟机的时间加上等待时间,纳秒级别
将当前线程添加到队列中
定义一个 failed 变量,该变量用来表示该线程最终的结果是否拿到锁,开始为 false,因为刚开始没拿到
开始进行自旋
当前线程查看自己的上一个结点是不是头结点
如果是,再判断自己能不能抢到锁
如果抢到了锁,调整线程队列,将自己设为头结点,并且断开旧头结点与队列的连接,让其被 gc 回收,然后修改 failed 变量为 true,代表拿到了锁
如果上一个结点不是头结点,或者,是头结点但抢不到锁
计算当前线程还剩下多少等待时间,使用最大等待时间点减去当前时间点
如果等待时间小于等于 0,那就代表超时了,返回 false
如果没有超时,判断这个线程需不需要 park 掉
如果自旋已经两次了,且剩余时间还足够,那就 park 掉,这个 park 会在指定一段时间后恢复回来,这一段时间就是指线程剩余的时间
如果在剩余时间没有被主动唤醒,自己也会醒,但下一轮的自旋就会被判断超时了
如果在剩余时间被主动唤醒,唤醒的动作有两种,一种是 unpark,一种是 interrupt
判断唤醒是不是 interrupt 方式,如果是,那就直接抛出异常
最后执行 finally 里面的判断
如果 failed 变量为 true,也就是该线程一直获取不到锁,就会执行 cancelAcquire 方法
spinForTimeoutThreshold
下面我们来看一下,是如何判断剩余时间是否足够进行 park 的
从代码上来看,只要大于 spinForTimeoutThreshold 就 i 是剩余时间足够进行 park
而这个 spinForTimeoutThreshold 具体的数值为 1000,也就是 1000 纳秒
parkNanos
这个方法就是在指定时间内将线程 park,并且等超时了之后,就会恢复
可以看到,里面使用了内存屏障来保证线程安全的
cancalAcquire
这个方法就是当获取不到锁的时候,就会执行
获取不到锁,那当然就是将线程从队列中弹出呀,并且如果线程前面有一些其他线程也是取消状态的,顺便也要弹出,即不但要取消自己,还要取消前面的一些不要的线程
源码如下
private void cancelAcquire(Node node) {
//判断要移除的线程在不在
if (node == null)
//不在就直接返回
return;
//将结点里面的线程删除
node.thread = null;
// Skip cancelled predecessors
//获取结点的上一个结点
//下面就是针对前面的一些被取消的结点的操作
//这些结点也是要被删除的
Node pred = node.prev;
//循环判断上一个结点的状态是否大于 0,如果大于 0 代表就要被删除
while (pred.waitStatus > 0)
//进入到这里就判断上一个结点要删除了
//所以让 Node 与上上个结点连接,
//同时将 pred 变为上上个结点,为了下一次循环
//这一步可以视为两步
//pred = pred.prev
//node.prev = pred
node.prev = pred = pred.prev;
//循环在这里就结束了,当初就是在这里卡了好久
//现在已经让正常的结点 node.prev 为正常的结点
//但还没有进行断开,下面的操作就是断开的
//pred 此时就是正常的结点
//predNext 就是前面一个被取消的结点
//下面只需要对这个 predNext 删除就行,因为这段的线程都是取消的
Node predNext = pred.next;
//将线程状态改为 cancelled
node.waitStatus = Node.CANCELLED;
//如果线程是尾结点
//那么就 CAS 将尾结点换成 Pred,也就是此时尾结点变成了 pred
if (node == tail && compareAndSetTail(node, pred)) {
//然后 CAS 将 pred 后面的线程全部删除,改为 Null
//这里就完全断开了需要删除的结点,并且正常的结点成为了尾结点
compareAndSetNext(pred, predNext, null);
评论