AQS源码阅读记录

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)使用acquire/release(模板)方法对外提供功能。使用一个双向队列和指向队列两端的head/tail指针来实现线程的有序排队,

ps:head指针指向的第一个node为傀儡node,不包含真实数据,通常对应正在运行的线程

1.void acquire(int)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

该方法首先调用被子类覆写的tryacquire方法,根据自定义逻辑判断资源(state,volatile变量)能否被获取。若成功,则相应线程直接往下执行;若失败则按照AQS中已经定义好的逻辑进行排队等候,包括以下步骤:

1.addWaiter(Node.EXCLUSIVE)   将当前线程作为一个独享节点加入AQS队列的尾部进行排队

2.acquireQueued   确保排队等待后,能在适当的时机被唤醒(head中的线程释放资源后唤醒后面node)

3.前两步执行时是不响应中断的,因此后续需要判断是否存在中断信号,若有则补上中断

2.tryAcquire(arg)

以可重入锁ReentrantLock的实现为例。ReentrantLock中有一个继承了AQS的抽象静态内部类Sync,该类中的nonfairTryAcquire方法如下,注意在最后设置state的值,防止重排序:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state为0时代表资源可用,此时CAS设置state为目标值
            if (c == 0) {
                //直接尝试修改state,无视排队中的线程,非公平
                //公平锁的实现会判断,是否有线程在排队.若有则当前线程进队排队,若没有才会尝试修改state
                if (compareAndSetState(0, acquires)) {
                    //成功则设置当前线程占用资源
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //若state不为0说明资源已被占用,查看是否当前线程占用
            else if (current == getExclusiveOwnerThread()) {
                //若是,则重入。并增加state的值(释放时,只有该值减小至0时资源才真正释放)
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //最后设置volatile变量,防止前面的代码被重排序到后面
                setState(nextc);
                return true;
            }
            return false;
        }

3.addWaiter(Node.EXCLUSIVE) 

将当前线程封装在互斥的node中,并将node设置到队尾

 
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试快速将自己作为尾节点添加进队列
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //失败则自旋添加
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 初始化头节点,head和tail均指向该节点,此时数据为空,后续获得资源且正在运行的线程会包含在内
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //自旋将自己设置为尾节点
                node.prev = t;
                //若这里切换线程,则下一步CAS会失败,因此若CAS成功,说明t以及当前节点的前继节点必然指向之前的尾节点
                if (compareAndSetTail(t, node)) {
                    //若这里切换线程,t仍然指向尾节点,即使此时tail已经指向其他节点,也并不影响当前线程视角中尾节点与当前线程节点的连接
                    t.next = node;
                    return t;
                }
            }
        }
    }

4.acquireQueued

确保当前线程在队列中排队阻塞后,其前驱节点的状态为signal,节点在释放资源时会判断自己的状态,并以此决定是否唤醒后继节点

    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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }//尝试失败后确定是否阻塞当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //若当前线程在队列中等待超时或者被中断,则取消排队
            if (failed)
                cancelAcquire(node);
        }
    }

5.shouldParkAfterFailedAcquire

尝试使当前节点前驱waitStatus为signal,尝试失败则返回false,返回至acquireQueued方法继续自旋


    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点waitStatus为signal,直接返回true,代表可以安全的阻塞。
             */
            return true;
        //下面操作始终返回false,即需要重新自旋尝试
        if (ws > 0) {
            /*
             * 若前驱节点waitStatus为cancelled,从队列中断开该节点,并重复这一操作
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //尝试设置前驱节点waitStatus为signal
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

6.parkAndCheckInterrupt

   private final boolean parkAndCheckInterrupt() {
        //将当前线程阻塞到aqs对象上
        LockSupport.park(this);
        //被唤醒时返回线程的中断状态,若存在中断信号,后续会补上中断位的设置
        return Thread.interrupted();
    }

这里返回的中断位最终会在acquire方法中重新设置中断位,acquire方法的上层调用处可能会有类似while(!this.isInterrupted())的代码会用到中断位。(这里为何要重设中断位,执行全程不管中断位不行么?)

因为park方法在中断置位时会直接往下执行,没有阻塞的效果,且不会将中断位复位。因此当中断位置位时,park方法会直接返回,进而导致节点无限自旋。所以需要Thread.interrupted()方法将中断位复位。

另外,wait与park不同,中断位在wait前设置时,执行到wait之后会抛出(中断)异常;中断位在wait中设置时,并不能马上响应中断,必须等其他线程释放锁,当前线程才能从wait处继续执行,此时会抛出(中断)异常。无论哪种情况,抛出异常后都会走catch块代码,不会立即释放锁(除非将异常抛到上层)

7.cancelAcquire

acquireQueued方法循环中出现中断或者超时,会执行该方法


    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        //为后续CAS设置pred的next指针做准备(predNext为原始值)
        Node predNext = pred.next;

        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                //确保pred状态为signal,连接当前节点的前后节点(非cancelled状态)
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                //无法确保则直接唤醒后继节点,后续节点在被唤醒后会确保其pred节点为signal状态,然后再park
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

8.release

调用子类实现的tryRelease,若返回true,且头节点waitStaus值不为0,说明后面有节点等着被唤醒。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

9.unparkSuccessor

唤醒后继节点,这里唤醒后会再次在acquireQueued方法中循环自旋调用tryAcquire获取资源,若被唤醒的节点和head节点中间存在cancelled节点,会在当次循环中在shouldParkAfterFailedAcquire方法中从队列中移除这些节点,下次循环即可调用tryAcquire方法尝试获取资源。若此次获取失败了,说明可能是另一个线程在当前线程从阻塞中醒来之后、调用tryAcquire方法之前抢占了资源(非公平锁未排队)。


    private void unparkSuccessor(Node node) {
        //这里为何要清除waitStatus有点不明白,ws<0说明node并非被取消的节点,那么node应该是头节点,如果node的后继被唤醒后抢到了资源,那么node将出队;若没有抢到,node的后继在park前会将其前驱node再设置成signal。难道是在node的后继抢资源的过程中避免对其重复调用unpark?
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

       //从尾到头找到最后一个(离当前节点最近的)非cancelled的节点,并唤醒(中间可能有cancelled的节点)
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

唤醒后继之前要清除前驱节点的waitStatus,猜想应该是避免对同一后继节点重复调用unpark,这样会导致后继节点无法阻塞。如果对运行状态下的线程调用unpark,那么线程在第一次park自己的时候会消耗掉前面unpark提供的permit,继续往下运行(自旋),而不是阻塞自己。

10.doAcquireShared

与上述tryAcquire的主要区别是:成功抢到资源后不仅更新头节点,还要决定是否扩散共享状态,即唤醒后续共享状态的节点。涉及方法:setHeadAndPropagate

        Node h = head; // Record old head for check below
        setHead(node);
        /*
         若还有剩余资源,或者其他情况(暂时没看懂)
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //后续节点处于共享状态,则继续唤醒
            if (s == null || s.isShared())
                doReleaseShared();
        }

11.doReleaseShared

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    //与release不同,必须要先将node的waitStatus清0,也没看懂为何
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //如果ws为0,说明后续没有要唤醒的节点,那么为什么还是设置h的状态为PROPAGATE?若后来有节点排队,那么后来的节点也会在park之前将h的状态设置为signal,不管h的状态是0还是PROPAGATE
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

猜你喜欢

转载自blog.csdn.net/qq_37720278/article/details/82561359