高并发之JUC——AQS源码深度分析,有你不得而知的条件等待队列(二)

在这里插入图片描述> 上一篇分析了关于AQS独占锁的执行流程和源码。在AQS中不仅涉及了独占锁,还涉及了共享锁及带超时时间的共享锁、中断共享锁。本文就讲解上述锁的获取锁和释放锁的原理。

AQS获取共享锁acquireShared

共享锁,顾名思义就是多个线程可以共享同一把锁,在JUC下面如CountDownLatch就是基于共享锁实现的。

那么了解了共享锁是什么,那么先来看下它是如何获取锁的。

源码

共享锁获取锁和独占锁获取锁在AQS中的逻辑是基本一致的,流程图可以参考我上一篇中关于AQS独占锁加锁的流程图。接下来先看下执行代码:

acquireShared:

public final void acquireShared(int arg) {
    // 获取锁方法由各自子类实现
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

下面的方法和独占锁一样,在上一篇中已经对代码有详细说明,唯一与独占锁处理逻辑不一样的地方在于setHeadAndPropagate方法,独占锁此处只是将获取到锁的节点设置为了头节点,但是共享锁不仅将获取到锁的节点设置为了头节点,还会唤醒下一个节点。那么我们重点看下setHeadAndPropagate这个方法。

doAcquireShared:

private void doAcquireShared(int arg) {
    // 将节点添加到等待队列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取当前节点的前继节点,如果为头节点,那么就有机会获取锁。
            final Node p = node.predecessor();
            if (p == head) {
                // 竞争锁,当返回值<0的时候,那么就是获取锁失败,反之则成功获取锁。
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 这块是和独占锁不一致的地方。独占锁只是将获得锁的节点直接设置为头节点,这里不仅设置为头节点,还向下传播。
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate:这个方法首先将获取到锁的节点设置为头节点,然后唤醒下一个节点。

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) {
            // 获取到锁的节点的next节点是共享锁节点,那么唤醒后续节点
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

doReleaseShared:唤醒下一个节点,之后这个节点就有机会去竞争锁了。

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
                    // 释放h.next节点
                    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、线程A请求锁,首先去获取锁。若成功获取到锁,直接返回,否则执行doAcquireShared。假设当前没有线程持有这把锁,且线程A成功获取到这把锁,运行线程A。

2、此时线程B请求加锁,同样也会尝试获取锁,那么正常此时获取锁失败(A线程持有了这把锁)。执行doAcquireShared。

3、doAcquireShared是一个死循环,在doAcquireShared中将请求B加入等待队列,在加入前,等待队列为空,加入后,等待队列如图1所示。

4、此时B线程会判断在等待队列中B线程的前继节点是不是head,如果是,则在此获取锁。目前的情况假设A还没有释放线程,B依然拿不到锁。那么进入到下一次循环,因为doAcquireShared是一个死循环,拿不到锁就一直阻塞在这个方法中。

5、此时假设A还未释放锁,那么当线程C请求过来的时候,同样会经历步骤2和3,然后依然会判断线程C对应的节点的前继节点是不是头结点,此时肯定不是头结点,C的前继节点是B节点,那么会执行shouldParkAfterFailedAcquire方法。

6、在shouldParkAfterFailedAcquire方法中,首先会判断C节点的前继节点(也就是B节点)的状态是不是SIGNAL(表示存在后继节点需要被唤醒),此时节点B的状态为0(初始化状态),当状态为0是,会将B节点的状态置为SIGNAL,并且返回false。

7、此时C线程请求返回到doAcquireShared方法,因为返回了false,所以根据doAcquireShared方法的逻辑,会进入下一次循环(因为是死循环)。此时C执行下一次循环后,又会执行到shouldParkAfterFailedAcquire方法(因为C节点的前继节点还不是head),但此时执行shouldParkAfterFailedAcquire方法会返回true(因为前继节点B的状态在上面已经设置为SIGNAL)了。返回true后,在doAcquireShared方法中会执行parkAndCheckInterrupt方法,阻塞当前线程,也就是线程C被阻塞挂起了,等待其他线程来解阻塞。

综上,此时线程A持有锁,线程B在自旋,状态为SIGNAL,并且一直尝试获取锁,线程C阻塞挂起,状态为0。

AQS释放共享锁

AQS释放共享锁和上一篇讲的独占锁处理是基本一致的,都是唤醒后续节点。

1、假设此时A线程执行结束,释放锁。当A释放锁成功后,会执行doReleaseShared方法。
2 、在doReleaseShared中,主要是释放下一个节点,让它有机会去竞争锁,这里也就是唤醒B线程。

AQS可打断共享锁

acquireSharedInterruptibly:这个方法与共享锁获取锁逻辑基本一致,唯一不同的是在尝试获取锁之前,会判断当前线程的打断状态,如果当前线程线程被打断,那么直接抛出异常。

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    // 判断当前线程是否被打断,如果打断,那么直接抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

释放锁的流程和共享锁是一致的。

AQS超时模式独占锁

在获取锁的时候可以传入一个超时时间,当自旋等待的时间超过传入的等待时间时,那么返回获取锁失败,然后直接取消该线程所属节点(也就是将在等待队列中的该节点的状态设置为canceled)。

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                // 会判断等待是否超时,如果超时就直接返回,不会在这里自旋等待。
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            // 如果获取锁失败,可能是等待超时,那么直接将该节点设置为取消状态。
            if (failed)
                cancelAcquire(node);
        }
    }

重点看下上面带注释的两行代码,分别为判断等待时间是否超过传入的等待时间,也就是等待是否超时,如果等待超时,那么直接返回获取锁失败,并取消该节点。其余的处理和独占锁获取锁流程是完全一致的。

条件等待队列

我们先看下面的代码实例:


    static ReentrantLock reentrantLock = new ReentrantLock();
    static Condition condition = reentrantLock.newCondition();

    static int a = 0;

    public static void add() {
        reentrantLock.lock();

        while (a == 0) {
            try {
                a++;
                System.out.println("add, a="+a);
                condition.signal();
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        reentrantLock.unlock();
    }

    public static void sub() {
        reentrantLock.lock();

        while (a == 1) {
            try {
                a--;
                System.out.println("sub, a="+a);
                condition.signal();
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        reentrantLock.unlock();
    }
    
    public static void main(String[] args) throws Exception{

        new Thread(new Runnable() {
            @Override
            public void run() {
                add();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                sub();
            }
        }).start();

    }

上面的代码很简单,是个简答的生产者消费者模型。其中ReentrantLock是独占锁,这个我们后面会有单独的文章来讲解。下面想说的是reentrantLock.newCondition(),那么Condition是什么呢?下面我们就来说一下。

Condition是AQS中的另一个队列,之前我们说过的队列是AQS等待队列,现在我们要说的也是AQS类中的一个队列,叫条件等待队列。首先当前线程要先获取到锁,当满足指定条件(程序员自己指定)时,可通过signal来唤醒其他线程来获取锁,然后通过await方法将当前线程阻塞,这个就是条件队列的作用,也就是可以切换不同线程来获取同一把锁。在AQS的源码中定义如下:

public class ConditionObject implements Condition, java.io.Serializable {

        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        // 头节点
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        // 尾节点
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }

        // Internal methods

        /**
         * Adds a new waiter to wait queue.
         * @return its new wait node
         */
         // 向队列中加入节点,状态为CONDITION
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

        /**
         * Removes and transfers nodes until hit non-cancelled one or
         * null. Split out from signal in part to encourage compilers
         * to inline the case of no waiters.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

        /**
         * Removes and transfers all nodes.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

        /**
         * Moves the longest-waiting thread, if one exists, from the
         * wait queue for this condition to the wait queue for the
         * owning lock.
         *
         * @throws IllegalMonitorStateException if {@link #isHeldExclusively}
         *         returns {@code false}
         */
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        ... ...
    }

上面代码就是在AQS源码中定义的条件等待队列,包含了头节点和尾节点两个Node,这里其实就是一个双向链表。至于signal和await方法的原理,我将在后面的文章中专门来详细讲解,这里只需知道在AQS中不仅定义了一个等待队列,还定义了一个条件等待队列来供子类实现更多的功能(唤醒其他线程,阻塞当前线程)。

最后

本文包括上一篇AQS文章基本把AQS的总体结构及源码都介绍了一下,大家应该对AQS有了一个总体印象。接下来的本系列文章将分析JUC包下面的具体实现类,如ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore等。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值