Java多线程 之 CountdownLatch AQS

​     目录

                1、背景介绍

                2、运行实例

                3、源码分析

                4、AQS架构模型


一、背景介绍

CountDownLatch是一个基于AQS框架实现的同步工具类,其作用是允许一个或多个线程等待其他一个或多个线程执行完成之后才继续执行,否则,将一直处于阻塞状态。

CountDownLatch核心使用一个同步计数器进行实现,计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减1。当计数器的值为0时,表示所有的线程都已经完成任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

二、运行实例

下面先通过一个简单的实例看一下CountDownLatch的工作过程。

/**
 * @author: liuheyong
 * @create: 2019-09-15
 * @description:
 */
public class TestCountDownLatch {
​
    public static void main(String[] args) {
        testCountDownLatch();
    }
​
    public static void testCountDownLatch() {
        int threadCount = 10;
        final CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println("线程===" + Thread.currentThread().getId() + "===开始执行");
                try {
                    Thread.sleep(1000);
                    System.out.println("线程===" + Thread.currentThread().getId() + "===执行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程已经执行完毕,main开始执行后续任务。");
        System.out.println("子线程已经执行完毕,main开始执行后续任务。");
        System.out.println("子线程已经执行完毕,main开始执行后续任务。");
    }
}

这里定义了CountDownLatch,构造方法传入参数为10,表示将要开启10个子线程去执行子任务,通过CountDownLatch.await方法,进行阻塞main线程,将main线程放入CountDownLatch内部构造的CLH同步队列当中,在CountDownLatch10个子线程全部执行完之前,main线程将一直处于阻塞状态,直至CountDownLatch10个子线程任务全部执行完毕,main接着继续执行后续内容。

以上是个比较简单的实例,下面是个稍微复杂点的实例,后续再对CountDownLatch内部源码实现进行分析。

/**
 * @author: liuheyong
 * @create: 2019-09-15
 * @description:
 */
public class TestCountDownLatch2 {
​
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdlResult = new CountDownLatch(1);
        final CountDownLatch cdlPlayer = new CountDownLatch(4);
        for (int i = 0; i < 4; i++) {
            Runnable runnable = () -> {
                try {
                    System.out.println("运动员" + Thread.currentThread().getName() + "正在等待裁判发布口令");
                    cdlResult.await();
                    System.out.println("运动员" + Thread.currentThread().getName() + "已接受裁判口令");
                    Thread.sleep((long) (Math.random() * 3000));
                    System.out.println("运动员" + Thread.currentThread().getName() + "到达终点");
                    cdlPlayer.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
            service.execute(runnable);
        }
        try {
            Thread.sleep((long) (Math.random() * 3000));
            System.out.println("裁判" + Thread.currentThread().getName() + "即将发布口令");
            cdlResult.countDown();
            System.out.println("裁判" + Thread.currentThread().getName() + "已发送口令,运动员正在赛跑当中...");
            cdlPlayer.await();
            System.out.println("所有选手都已赛跑完成");
            System.out.println("裁判" + Thread.currentThread().getName() + "计算成绩排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}

这是一个运动员赛跑的情景模拟,定义了两个CountDownLatch实例,cdlResult只有一个线程表示裁判进行发布口令和统计最终结果,cdlPlayer创建4个线程代表4个运动员同时进行赛跑,在for循环的run方法中调用cdlResult.await(),表示四个运动员都在等待裁判发布口令,这时四个子线程将会被阻塞在cdlResult的内部的CLH同步队列当中,只有cdlResult的同步状态变量为0的时候,四个线程才开始执行后续跑步过程。赛跑的过程中调用cdlPlayer.countDown()进行cdlPlayer同步状态变量的递减操作。在cdlResult.countDown()执行后cdlResult的同步状态变量减为0,运动员开始赛跑,cdlPlayer.await()将main线程阻塞到cdlPlayer内部的同步队列当中,当所有的运动员任务全部完成之后main线程开始执行后续操作。

三、源码分析

CountDownLatch的实现开头我们说过是基于AQS进行实现的。首先我们看其构造方法。

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
​
    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) {
            return (getState() == 0) ? 1 : -1;
        }
​
        protected boolean tryReleaseShared(int releases) {
            // 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;
            }
        }
    }
​
    private final Sync sync;

可以看到构造方法传入的参数count最终赋值给了继承了AbstractQueuedSynchronizer的Sync类,这个Sync类以内部类的形式被创建,其实就是自定义的同步器,最终以成员变量的形式等待使用。

在这两个实例中我们主要使用了CountDownLatch的countDown和await方法,接下来我们就看这两种方法是怎么实现的。

public void countDown() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

countDown直接调用sync的releaseShared方法,默认参数arg为1。

tryReleaseShared尝试去释放共享资源,这个具体释放逻辑需要实现者根据需要去实现。

 protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    
    //CountDownLatch的实现方式
    protected boolean tryReleaseShared(int releases) {
            // 递减计数;递减到零时发出信号
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

CountDownLatch的实现方式并不复杂,getState然后CAS自旋的方式进行赋值state同步变量。然后比较减去1之后的nextc是否为0。如果为0就可以唤醒后续的线程了。

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;  
                    //唤醒后继              
                    unparkSuccessor(h);
                }
                else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

Node.SIGNAL表示结点Node的状态:

需要补加一点的是共享模式下的releaseShared()拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。这一点是和独占模式下释放资源不同的一点。

    • CANCELLED(1):表示当前Node已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

    • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL,后继结点为状态为0。

    • CONDITION(-2):表示结点在Condition上等待,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

    • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继以后的后继结点。

    • 0:新结点入队时的默认状态。

而至于await方法,则是直接调用sync的acquireSharedInterruptibly方法,传入参数为1。

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

这是一种在共享模式下、响应中断地获取共享资源方式。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程响应中断。

首先Thread.interrupted判断线程是否被中断,如果中断则直接抛出异常结束。tryAcquireShared依然是AQS的顶层接口,需要实现者自己去实现,CountDownLatch的实现逻辑如下。

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

如果返回为负,表示获取失败,失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

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;
                    }
                }
                //状态检查是否k可以park进行park操作
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            //如果fail为true,取消获取操作
            if (failed)
                cancelAcquire(node);
        }
    }

执行逻辑注释已经标明,最终执行park操作之后线程就进入阻塞状态了。

四、AQS架构模型

接下来我们回过头来看下基于本文两个实例执行过程中放入同步队列的整体架构模型是什么样的。

首先我们看下AQS中Node类的结构图示,因为同步队列中的每个元素都是一个Node结点,他们之间相互指向这种逻辑结构构成了同步队列。

在本文中第一个实例中,只有一个CountDownLatch实例,所以只会存在一个CLH同步队列。在这个方法执行到await后中只把main线程放入到了CLH队列中。如下:

head、tail、state都是AQS中定义的成员变量,在这个实例中只有一个head结点和一个tail结点,为同一个结点,都是线程main。

AQS架构图大致如下:

在第二个实例当中,我们使用了两个CountDownLatch实例,所以会存在两个CLH同步队列。在程序的执行过程中,第一个队列cdlResult.await阻塞四个子线程,把他们放入到cdlResult同步队列当中,在后续执行过程中,cdlPlayer.await进行阻塞main线程,把main线程放入cdlPlayer同步队列当中。

初始状态如下:

当子线程都被阻塞放入cdlResult同步队列当中,cdlPlayer.await还没有执行到的时候,结构如下:

此时,cdlResult同步队列结构如下:

当cdlPlayer.await执行完成,子线程开始执行任务,main线程被阻塞放进cdlPlayer同步队列当中,结构如下:

cdlResult同步队列只保留最后一个Node@701引用,cdlPlayer同步队列只有一个main线程。

本篇并没有从AQS开始分析其理论知识,而是从CountDownLatch实例入手逐步深入源码探究其执行原理,进而了解实现其功能的背后框架支持——AQS框架,由浅入深,逐步探究其实现原理,但是AQS实现原理并不仅仅局限于本文范围所讲。而且,基于AQS实现的同步工具非常之多,像大名鼎鼎的ReentrantLock、ReentrantReadWriteLock和Semaphore等。线程等待队列也绝非CLH同步队列一种,还有Condition等待队列(一个或多个)。而且还需注意区分Object对象的Synchronize对象锁及同步队列和AQS同步队列的区别,一一细节,不再列举,本文暂且抛砖引玉,愿日日新,日日有所进步!

 

更多内容持续更新中,感兴趣的朋友请移步至个人公众号,谢谢支持😜😜......

公众号:wenyixicodedog

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值