CountDownLatch、ReentrantLock源码解析

779 阅读5分钟

这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

1.AQS

因业务中在用多线程并行执行代码块中会用到CountDownLatch来控制多线程之间任务是否完成的通知,最近突然想去看一下CountDownLatch在await及唤醒是如何实现的,便开始了阅读源码及查阅资料,然后打开了一个新大门。发现它是基于AbstractQueuedSynchronizer(下文简称AQS)框架实现的。

所以我们先了解AQS是干什么的。它提供的功能可以概括为两点:获取资源,如果获取失败加入等待队列并且休眠该线程;释放资源,同时检查是否符合唤醒等待队列中线程的条件,如符合就唤醒线程继续执行。

ReentrantLock,CountDownLatch,ReentrantReadWriteLock等都是基于这个类做的。 它提供了几个方法让子类进行复写以实现各自的功能:

tryAcquire(int arg)//尝试获取独占资源
tryRelease(int arg)//尝试释放独占资源
isHeldExclusively()//判断当前线程是否获取独占资源

tryAcquireShared(int arg)//尝试获取共享资源
tryReleaseShared(int arg)//尝试释放共享资源

这5个方法可以分为两类独占类接口(前3个)和共享类接口(后两个),因为一个子类中一般只需要(也应该如此,ReentrantReadWriteLock同时需要独占和共享,但也是分成两个类来实现的)实现其中一类方法簇,所以作者并没有把他们写成抽象方法,这样对AQS的子类更友好。

这里对AQS独占和共享概念解释一下,这是对AQS的资源(也就是state字段)的描述。即在满足可以获取资源的条件后,在队列中的等待的线程是唤醒一个(一个线程独占)还是说等待的线程都可以唤醒(共享)。

对于AQS的state字段,也就是线程抢夺的资源,不同的子类有不同的定义,标题中提到的CountDownLatch和ReentrantLock正好是对state有不同的概念,看到对这两个类的分析,大家就自然清楚了。AQS内部实现了对资源的获取,释放逻辑,让子类实现的主要是尝试获取和释放资源的场景逻辑。

下面对AQS内部逻辑进行分析,不过其子类根本不用关注这些逻辑,这些方法主要实现了:获取资源,获取失败后加入队列并且让线程阻塞,释放资源满足线程监视的条件后,从等待队列中剔除并唤醒相应的线程。,所以在看CountDownLatch,ReentrantLock代码时可以不细看这个方法的实现,先了解每个方法的作用,搞清楚子类的逻辑后,可以在慢慢研究AQS内部控制。

1.1 获取共享资源

//AQS源码
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

获取资源的代码,首先调用tryAcquireShared 这个方法来判断是否可以获取到锁(这个方法交友子类实现其判断是否可以获取的逻辑),如果可以获取任何代码都不执行,如果不可以获取就进入if执行doAcquireShared(arg)

 /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);//1
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {//2
                    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())//3
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

因为不能获取到资源,所有线程要加入到等待队列中并且线程进入阻塞状态。这里要说一下AQS的等待队列是由一个双向链表实现的,节点Node有前后节点,当前线程,等待状态值这几个字段组成。所以1处就是在链表中加入一个共享模式的节点。这个方法我就不写了,里面用了CAS+自旋的无锁的方式 确保在多线程下可以正确插入。

在2处判断当前节点是不是当前队列中第一个节点(head头节点就是一个空节点,head指向的下一个节点,才是等待队列中的第一个节点线程),如果是第一个,在次检测一下是否可以获取到锁,如果可以获取锁了,该线程就直接从队列里剔除,继续执行了(可能有人问之前不是尝试获取过一次吗,这里干嘛还在尝试,我认为是为了减少线程无价值的状态变更吧),如果依旧不能获取资源,进行3处,shouldParkAfterFailedAcquire这个方法是判断该线程该不该进行休眠

    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) {//2
            /*
             * 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;
    }
 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

可以看到通过LockSupport.park把线程置成阻塞状态。

1.2 释放共享资源

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
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 (;;) {//1
            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处是一个死循环,如果当前节点释放后,会一直便利直到队列为空或者进入不可唤醒状态

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

释放共享节点AQS从队列中剔除并且uppark该线程。

2.CountDownLatch实现

CountDownLatch的核心就是实现AQS的Sync内部类

 private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {//1
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {//2
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

可以看到1,2处实现了tryAcquireShared和tryReleaseShared方法,结合上面AQS源码分析应该很容易明白了。

    public CountDownLatch(int count) {//CDL类构造方法
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
    
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    public void countDown() {
        sync.releaseShared(1);
    }

可以看到await方法调用的AQS的方法,该方法 if (tryAcquireShared(arg) < 0) 执行尝试调用的方法,这个由Sync实现,其实现就判断当前state是否等于0,等于说明coutnDown调用初始化的次数,不用阻塞。countDown也就会对state-1在Sync使用CAS进行了处理。

3.ReentrantLock

可重入锁提供了公平与非公平锁两种模式,他们的唯一区别就是在抢占锁的顺序,所以在内部实现的公平与非公平也是在lock()方法有所区别,公平模式下,有一个FIFO的队列进行排序。

这里以非公平锁为列

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

这里state为0表示当前没有线程持有该锁,>0表示有线程持有该锁,因为是可重入,所以当前线程lock一次,state就会+1,如下代码1处所示

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;//1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可重入锁使用的是AQS的独占模式,在细节上有些不同,AQS也是有两套方法,整个逻辑是一致的,不同点

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

在唤醒等待进程这块,只会唤醒一次AQS队列的第一个节点。在调用unlock方法时,会使用LockSupport.unpark()方法唤醒队首线程