JUC中的AQS底层详细超详解( 二 )


AQS详细资源获取流程1. tryAcquire尝试获取资源AQS使用的设计模式是模板方法模式 。
具体代码如下:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 发现中断过,则触发中断异常 selfInterrupt();}即AQS抽象基类AbstractQueuedSynchronizer给外部调用时,都是调的acquire(int arg)方法 。这个方法的内容是写死的 。而acquire中 , 需要调用tryAcquire(arg) ,  这个方法是需要子类实现的,作用是判断资源是否足够获取arg个
(下面部分代码注释选自: (2条消息) AQS子类的tryAcquire和tryRelease的实现_Mutou_ren的博客-CSDN博客_aqs tryacquire )
ReentrantLock中的tryAcquire实现这里暂时只谈论一种容易理解的tryAcuire实现,其他附加特性的tryAcquire先不提 。
里面主要就做这几件事:

  1. 获取当前锁的资源数
  2. 资源数为0 , 说明可以抢,确认是前置节点是头节点,进行CAS试图争抢,抢成功就返回true , 并设置当前线程
  3. 没抢成功,返回false
  4. 如果是重入的,则直接set设置增加后的状态值,状态值此时不一定为0和1了
protected final boolean tryAcquire(int acquires){ final Thread current = Thread.currentThread(); int c = getState(); // state==0代表当前没有锁,可以进行获取 if (c == 0) { // 非公平才有的判断,会判断是否还有前驱节点,直接自己为头节点了或者同步队列空了才会继续后面的锁的获取操作 if (!hasQueuedPredecessors() //CAS设置state为acquires , 成功后标记exclusiveOwnerThread为当前线程 && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 当前占有线程等于自己,代表重入 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; // 出现负数,说明溢出了 if (nextc < 0) // throw new Error("Maximum lock count exceeded"); // 因为是重入操作,可以直接进行state的增加 , 所以不需要CAS setState(nextc); return true; } return false;}2.addWaiter 添加到等待队列当获取资源失败,会进行addWaiter(Node.EXCLUSIVE),arg) 。
目的是创建一个等待节点Node,并添加到等待队列
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; // 通过CAS竞争队尾 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 竞争队尾失败 , 于是进行CAS频繁循环竞争队尾 enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node()))tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }3. acquireQueued循环阻塞-竞争并在 "处于头节点时尝试获取资源->睡眠->唤醒“中循环 。
当已经跑完任务的线程释放资源时,会唤醒之前阻塞的线程 。
当被唤醒后,就会检查自己是不是头节点,如果不是,且认为可以阻塞,那就继续睡觉去了
(下面代码注释部分选自AQS(acquireQueued(Node, int) 3)–队列同步器 - 小窝蜗 - 博客园 (http://cnblogs.com) )
final boolean acquireQueued(final Node node, int arg) { // 标识是否获取资源失败 boolean failed = true; try { // 标识当前线程是否被中断过 boolean interrupted = false; // 自旋操作 for (;;) { // 获取当前节点的前继节点 final Node p = node.predecessor(); // 如果前继节点为头结点,说明排队马上排到自己了,可以尝试获取资源,若获取资源成功,则执行下述操作 if (p == head && tryAcquire(arg)) { // 将当前节点设置为头结点 setHead(node); // 说明前继节点已经释放掉资源了,将其next置空,好让虚拟机提前回收掉前继节点 p.next = null; // help GC // 获取资源成功,修改标记位failed = false; // 返回中断标记 return interrupted; } // 若前继节点不是头结点 , 或者获取资源失败,// 则需要判断是否需要阻塞该节点持有的线程 // 若可以阻塞 , 则继续执行parkAndCheckInterrupt()函数, // 将该线程阻塞直至被唤醒 // 唤醒后会检查是否已经被中断 , 若返回true,则将interrupted标志置于true if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())interrupted = true; } } finally { // 最终获取资源失败,则当前节点放弃获取资源 if (failed) cancelAcquire(node); } }4.shouldParkAfterFailedAcquire 检查是否可以阻塞该方法不会直接阻塞线程 , 因为一旦线程挂起 , 后续就只能通过唤醒机制,中间还发生了内核态用户态切换,消耗很大 。

推荐阅读