java多线程学习之【CountDownLatch】

目前卷文化盛行,为了增强面试能力,开始了无边无际的学习,无边界不是重点,重点是要深入1万米。言归正传,本文打算做一个多线程学习的系列文章,沉淀自我。


前言

本文主要是讲解CountDownLatch的概念,基本用法,使用场景,底层代码原理剖析。

一、CountDownLatch是什么?

按照源码注释,翻译成中文,就是以下内容:

  • 允许一个或多个线程等待其他线程正在执行的一组操作完成。
  • await方法阻塞当前线程直到由于countDown方法的调用将当前计数为0。
  • 计数为0之后,所有等待的线程被释放和任何后续调用的await立即返回。
  • 这是一个一次性的现象——计数不能重置。如果你需要重置计数的版本,考虑使用CyclicBarrier循环障碍。
  • 中文叫法很多,如 倒计时门栓,发令枪,闭锁,计数器。

二、使用方法

CountDownLatch 提供了一些方法:

方法说明
await()使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。
await(long timeout, TimeUnit unit)带超时时间的await()。
countDown()使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。
getCount()获得latch的数值。

三、应用场景

倒计时门栓是一个多用途的同步工具,可以用于许多目的。
初始化作为一个门或者门栓的开关。当所有线程调用await在门口等待,直到一个线程调用countDown且count等于0而被打开。
计数初始化为N,可以用来使一个线程等待N线程完成了一些行动,或一些行动已经完成N次。线程调用wait时,不需要计数达到零,它只是阻止任何线程继续运行await程序之后,直到所有线程可以通过。

3.1双门栓

首先是一个开始的信号,防止任何工人进行,直到司机准备继续;
第二是完成信号,允许司机等到所有工人已经完成了。

代码如下

package com.valley.juc.tools.countdownlatch;

import java.util.concurrent.CountDownLatch;

/**
 * @author valley
 * @date 2022/6/15
 * @Description 多门栓demo
 */
public class Driver {
    public static void main(String[] args) throws InterruptedException {
        int N =100;
        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(N);
        for (int i = 0; i < N; ++i) { // create and start threads
            new Thread(new Worker(startSignal, doneSignal)).start();
        }
        new Driver().doSomethingElse();            // don't let run yet
        startSignal.countDown();      // let all threads proceed
        doSomethingElse2();
        doneSignal.await();           // wait for all to finish

    }

    private static void doSomethingElse2() {
        System.out.println("doSomethingElse...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private  void doSomethingElse(){
        synchronized (this){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("don't let run yet");
    }
    static class  Worker implements Runnable{
       private final CountDownLatch startSignal;
        private final CountDownLatch doneSignal;
        Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
         this.startSignal = startSignal;
          this.doneSignal = doneSignal;
       }
        @Override
        public void run() {
            try {
                startSignal.await();
                doWork();
                doneSignal.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


         void doWork() {
             System.out.println("worker doWork...");
         }
    }
}


3.2单门栓

模拟并发,让并发线程一起执行。在并发请求HTTP接口的时候,合并多个返回结果,都是就是这种应用场景。

代码如下:

package com.valley.juc.tools.countdownlatch;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author valley
 * @date 2022/6/15
 * @Description TODO
 */
public class Driver2 {

    public static void main(String[] args) throws InterruptedException {
        int N = 100;
        CountDownLatch doneSignal = new CountDownLatch(N);
//        Executor e = Executors.newFixedThreadPool(2);
        List<String> list = new ArrayList<>();

        for (int i = 0; i < N; ++i) { // create and start threads
//            e.execute(new WorkerRunnable(doneSignal, i, list));
            new Thread(new WorkerRunnable(doneSignal, i, list)).start();
        }
        doneSignal.await();           // wait for all to finish
        AtomicReference<Integer> count = new AtomicReference<>(0);
        list.forEach(s -> {
            count.set(count.get() + Integer.parseInt(s));
        });
        System.out.println(count.get());
    }


    static class WorkerRunnable implements Runnable {

        private final CountDownLatch doneSignal;
        private final int i;
        private final List<String> list;

        public WorkerRunnable(CountDownLatch doneSignal, int i, List<String> list) {
            this.doneSignal = doneSignal;
            this.i = i;
            this.list = list;
        }

        @Override
        public void run() {
            doWork(i);
            list.add("100");
            doneSignal.countDown();
        }

        private void doWork(int i) {
            System.out.println("i'm " + i);
        }
    }
}

四、源码剖析

CountDownLatch 的源码在JUC并发工具中,相对简单:

  • 静态内部类继承AQS抽象类,所以底层基于AbstractQueuedSynchronizer 来实现的
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;
            }
        }
    }
  • CountDownLatch 构造函数中指定的count直接赋给AQS的state;
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
  • 每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的。
    public void countDown() {
        sync.releaseShared(1);
    }
    
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;
        }
    }
  • 而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。
 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);
        }
    }

五、和其他工具比较

  • CountDownLatch与Thread.join

CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减1操作,也可以在一个线程里调用n次执行减1操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。

  • CountDownLatch与CyclicBarrier

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,区别如下:
CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后再执行,而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。


总结

主要讲解CountdownLatch,后面重点讲AQS。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

blackoon88

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值