ReentrantLock 源码分析以及 AQS (一) (3)

PS:由 enq 方法,还有 setHead 方法,我们可以发现,头结点的线程总是为 null。这是因为,头结点要么是刚初始化的空节点,要么是抢到锁的线程出队了。因此,我们也常常把头结点叫做虚拟节点(不存储任何线程)。

shouldParkAfterFailedAcquire

以上是抢锁成功的情况,那么抢锁失败了呢?这时,我们需要判断是否应该把当前线程挂起。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取当前节点的前驱节点的 waitStatus int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果 ws = -1 ,说明当前线程可以被前驱节点正常唤醒,于是就可以安全的 park了 return true; if (ws > 0) { //如果 ws > 0,说明前驱节点被取消,则会从当前节点依次向前查找, //直到找到第一个没有被取消的节点,把那个节点的 next 指向当前 node //这一步,是为了找到一个可以把当前线程唤起的前驱节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //如果 ws 为 0,或者 -3(共享锁状态),则把它设置为 -1 //返回 false,下次自旋时,就会判断等于 -1,返回 true了 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

parkAndCheckInterrupt

如果 shouldParkAfterFailedAcquire 返回 true,说明当前线程需要被挂起。因此,就执行此方法,同时检查线程是否被中断。

private final boolean parkAndCheckInterrupt() { //把当前线程挂起,则 acquireQueued 方法的自旋就会暂停,等待前驱节点 unpark LockSupport.park(this); //返回当前节点是否被中断的标志,注意此方法会把线程的中断标志清除。 //因此,返回上一层方法时,需要设置 interrupted = true 把中断标志重新设置,以便上层代码可以处理中断 return Thread.interrupted(); }

想一下,为什么抢锁失败后,需要判断是否把线程挂起?

因为,如果抢不到锁,并且还不把线程挂起,acquireQueued 方法就会一直自旋下去,这样你的CPU能受得了吗。

cancelAcquire

当不停的自旋抢锁时,若发生了异常,就会调用此方法,取消正在尝试获取锁的线程。node 的位置分为三种情况,见下面注释,

private void cancelAcquire(Node node) { if (node == null) return; // node 不再指向任何线程 node.thread = null; Node pred = node.prev; //从当前节点不断的向前查找,直到找到一个有效的前驱节点 while (pred.waitStatus > 0) node.prev = pred = pred.prev; Node predNext = pred.next; //把 node 的 ws 设置为 -1 node.waitStatus = Node.CANCELLED; // 1.如果 node 是 tail,则把 tail 更新为 node,并把 pred.next 指向 null if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { int ws; //2.如果 node 既不是 tail,也不是 head 的后继节点,就把 node的前驱节点的 ws 设置为 -1 //最后把 node 的前驱节点的 next 指向 node 的后继节点 if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { //3.如果 node是 head 的后继节点,则直接唤醒 node 的后继节点。 //这个也很好理解,因为 node 是队列中唯一有资格尝试获取锁的节点, //它放弃了资格,当然有义务把后继节点唤醒,以让后继节点尝试抢锁。 unparkSuccessor(node); } node.next = node; // help GC } }

unparkSuccessor

这个唤醒方法就比较简单了,

private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; //从尾结点向前依次遍历,直到找到距离当前 node 最近的一个有效节点 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //把这个有效节点的线程唤醒, //唤醒之后,当前线程就可以继续自旋抢锁了,(回到 park 的地方) LockSupport.unpark(s.thread); }

下面画一个流程图更直观的查看整个获取锁的过程。

ReentrantLock 源码分析以及 AQS (一)

公平锁

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsgyp.html