AQS

前言

最近打算重新温习下JUC包,去年虽然有看过相关源码,但是理解的并不到位,最近不是特别忙,就打算花这个时间重新来梳理下。先从JUC中锁的实现的核心AQS说起。

AQS是一个抽象类,提供了一系列公共的方法,比较重要的操作有两个获取(acquire)和释放(release),其中每个操作有对应的一些扩展方法,比如超时、共享锁、可中断等。该类是JUC中各种锁的实现的基础,我们需要好好理解这个类,这样才能理解好锁的设计。

获取独占锁

 

    public final void acquire(int arg) 
        //与acquire不同的是,acquire方法是忽略中断的,而该方法是当遇到中断的时候会抛出异常
    public final void acquireInterruptibly(int arg)

上述两个方法都是获取独占锁的方法,我们以acquire(int arg)为例来分析。

如果想要获取独占锁,只需要调用acquire这个模板方法就好,AQS的设计使用了模板方法模式,acquire方法是使用final修饰的,不允许子类重写。

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

1.调用tryAcquire方法来尝试获取锁,在AQS的具体实现就是抛出一个异常,所以这个方法子类是需要重写的,其实不是很明白,为什么不把该方法设计为一个抽象方法。所以,在获取独占锁的时候,我们只需要重写这个方法,来实现我们何时可以获取独占锁的具体逻辑。

2.获取锁失败后,调用acquireQueued方法,该方法是AQS的私有方法,同样是无法重写的,该方法是在尝试获取独占锁失败后进入自旋尝试获取独占锁的过程,在自旋的过程中,为了防止无限循环,在大部分情况下会被挂起。

3.最后如果当前线程被中断过,需要调用selfInterrupt()方法来恢复中断标志。

  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);
        }
    }

我们看到这个方法只有当当前线程Node节点的前驱节点为头节点,并且获取锁成功后,才会退出循环,咋一看如果不满足这个条件就会导致无限循环发生,其实不是的,当一个线程进行自旋获取锁的时候,我们一定要注意避免无限循环的发生,别占着CPU不…。

当我们不满足跳出循环的条件的时候,我们会执行shouldParkAfterFailedAcquire,这个方法是判断当前线程节点是否是需要被挂起的。一旦满足条件就会调用parkAndCheckInterrupt方法来挂起当前线程,避免无限循坏。

  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

1.当前驱节点的waitStatus为SIGNAL时,返回true,当前线程需要被挂起了。

2.如果前驱节点的waitStatus大于0,即前驱节点被取消了,往前寻找到第一个waitSataus小于0的节点。

3.否则使用CAS将前驱节点的waitStatus置为SIGNAL。一旦在某个循环将前驱节点置为SIGNAL后,下一次循环再进入这个方法就会返回true,当前线程也会挂起,如果循环一直走shouldParkAfterFailedAcquire,并且方法返回true,就会导致线程挂起。接着看看线程挂起的方法。

   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

线程挂起方法不仅是私有方法,还使用了final修饰,毫无疑问,该方法无法被子类重写。

该方法很简单,调用LockSupport.park(this);将当前线程挂起,然后调用Thread.interrupted()返回线程的中断状态。实现是很简单,但是有没有想过为什么要调用Thread.interrupted()返回中断状态呢,这个方法可是会清除中断标志的。其实是必须要调用这个方法才行。LockSupport.park(this);与传统的Object.wait()不一样,一旦前者遇到了中断,会立即返回,继续执行,这个时候调用Thread.interrupted()会返回true,导致interrupted字段返回true(这样在获取锁之后,会调用selfInterrupt恢复中断状态),并且线程中断会被清除,然后进入下一次循环,如果依然执行到parkAndCheckInterrupt方法,此时中断标志已经清除了,所以线程还是会被挂起,但是如果不调用Thread.interrupted()清除中断标志的话,一旦线程被中断的话,当前线程会进入无限循环,所以这里必须要用Thread.interrupted()

释放独占锁

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

release方法和acquire方法一样是被final修饰的模板方法。

1.调用tryRelease方法尝试释放锁,这个方法同样是需要子类去重写的。如果释放锁失败的话,当前线程重入次数大于释放次数的时候,方法返回false,导致release直接返回false。

2.如果释放锁成功,通过调用unparkSuccessor方法去唤醒在CLH队列中被阻塞的线程。

   private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

唤醒阻塞的线程的方法依旧是个私有方法,不允许被重写。

1.当头节点的waitStatus小于0时,使用CAS将头结点的waitStatus状态置为0。

头节点可能是一开始新建的不带线程的Head(waitStatus为0),也可能当前线程获取锁的过程是经过自旋的,此时头节点就是包含当前线程的Node节点(waitStatus为-1 SIGNAL)。

2.取头节点的后继节点,如果后继节点不存在或者后继节点waitStatus大于0(后继节点被取消),那么就从tail往前需要第一个符合条件的Node节点(waitStatus<=0)。

3.如果获取的符合条件的节点不为空,唤醒这个节点的线程。

方法实现并不难,但是有没有想过2这里为什么要从tail开始向前查找?

这就要从Node入队方法说起,之前在获取锁的过程说过,当线程尝试获取锁失败后,会进入到CLH队列中,然后自旋获取锁。

addWaiter(Node.EXCLUSIVE);


    private Node addWaiter(Node mode) {
      //构造一个Node节点,当前线程设为Node的thread属性
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
      //获取当队尾Node
        Node pred = tail;
        if (pred != null) {
            //将队尾Node置为当前node的前驱节点
            node.prev = pred;
            //CAS设置当前node为队尾节点
            if (compareAndSetTail(pred, node)) {
                //CAS设置成功,将原队尾节点的后继节点置为当前节点
                pred.next = node;
                return node;
            }
        }
      //否则 一直循环直到成功插入到队列中
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
                 //如果尾节点为null,证明还没有头节点          初始化head和tail
            if (t == null) { // Must initialize
                        //CAS初始化一个空的头节点
                if (compareAndSetHead(new Node()))                    //头尾节点都指向新建的Node节点

                    tail = head;
            } else {
                
                             /*
             * AQS的精妙就是体现在很多细节的代码,比如需要用CAS往队尾里增加一个元素
             * 此处的else分支是先在CAS的if前设置node.prev = t,而不是在CAS成功之后再设置。
             * 一方面是基于CAS的双向链表插入目前没有完美的解决方案,另一方面这样子做的好处是:
             * 保证每时每刻tail.prev都不会是一个null值,否则如果node.prev = t
             * 放在下面if的里面,会导致一个瞬间tail.prev = null,这样会使得队列不完整。
             */
                node.prev = t;
                 //使用CAS将当前节点设置为尾节点

                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

我们看到在无限循环添加Node节点到CLH队列队尾的方法中,先执行node.prev = t;,然后才使用CAS将当前节点设置为尾节点,这样就保证了每时每刻tail.prev都不会为null,如果是放在CAS后设置,就可能会导致tail.prev = null,导致队列不完整。

结合这两个方法的实现,我们考虑一种情况

   if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }

当一个线程运行到这两行代码中间时,这个时候,pred的next指针是为空的,而且如果unparkSuccessor方法从头部向后遍历中,判断某个节点的next指针是否为空的逻辑恰好在这两行代码之间,而某个节点恰好又是pred节点,所以就找不到真正需要unpark的节点,所以就导致了死锁,也就是后续节点永远不可能被唤醒。所以必须要从尾巴向前进行遍历,找到真正需要unpark的节点。

获取共享锁

    public final void acquireSharedInterruptibly(int arg)
    public final void acquireShared(int arg)

两个都是获取共享锁的方法,不同在于一个是忽略中断的,一个是遇到中断就抛出异常的,一般我们在实际使用中是用的是遇到中断抛出异常的acquireSharedInterruptibly方法。

   public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

1.如果当前线程被中断了,抛出中断异常

2.调用tryAcquireShared方法尝试获取锁,该方法是需要子类重写的,返回值大于0代表成功获取共享锁。

3.如果获取共享锁失败,进入自旋获取锁的过程。

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

该方法与上述分析的acquireQueued方法没有太大差别,就不重新分析了,主要差别有两个,一个是遇到中断抛出异常。另外的就是当当前线程通过自旋获取到共享锁退出那块代码,之前的是setHead(node),现在调用的是setHeadAndPropagate(node, r);,这是为什么呢?

 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                
                doReleaseShared();
        }
    }

我们发现,当一个线程通过自旋获取到了共享锁之后,会调用doReleaseShared();尝试去唤醒在CLH中被阻塞的线程,并且将唤醒工作往下传递,在传递的过程中,其会判断被传递的节点是否是以共享模式尝试获取执行权限的,如果不是,则传递到该节点处为止。

释放共享锁

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

1.调用tryReleaseShared尝试释放锁,该方法也是需要子类重写的。如果失败,则直接返回false

2.如果释放成功,调用doReleaseShared()去释放CLH阻塞的线程。

   private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

1.如果CLH队列中没有元素,或者CLH队列中只有一个节点(第一个初始化的Head节点,意味着CLH中没有阻塞线程被唤醒),则直接break跳出循环,结束方法。

2.否则,就意味着CLH队列中有阻塞线程Node,查看头节点的waitStatus。

2.1如果是SIGANL状态,那么就使用CAS将节点waitStatus设置为0,直到成功,然后调用unparkSuccessor唤醒CLH中的阻塞线程。

2.2 如果waitStatus为0,设置为PROPAGATE。

总结

想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙,我们需要细细的去体会AQS的设计。
以上就是对AQS的一个简单分析,只是分析了几个重要的方法,其内部的数据结构并没有详细分析,之后会在分析各种JUC使用AQS来实现的锁中进行补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值