多线程(五) -- 并发工具(二) -- J.U.C并发包(四) -- CountDownLatch与CyclicBarrier的使用

1. CountDownLatch 倒计时锁

CountDownLatch主要提供的机制是当多个(具体数量等于初始化CountDownLatch时count参数的值)线程都达到了预期状态或完成预期工作时触发事件(countDown),其他线程可以等待这个事件来触发自己的后续工作。

值得注意的是,CountDownLatch是可以唤醒多个等待的线程的。

到达自己预期状态的线程会调用CountDownLatch的countDown方法,等待的线程会调用CountDownLatch的await方法。

如果CountDownLatch初始化的count值为1,那么这就退化为一个单一事件了,即是由一个线程来通知其他线程,效果等同于对象的wait和notifyAll,count值大于1是常用的方式,目的是为了让多个线程到达各自的预期状态,变为一个事件进行通知,线程则继续自己的行为。

1.1 原理:

  1. CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。
  2. 当线程使用countDown方法时,其实使用了tryReleaseShared方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。
  3. 当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。

1.2 示例一:

模拟要等教室里面所有人出来再关门:

public class TestCountDownLatch1 {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "go out");
                countDownLatch.countDown(); // 数量-1
            }, String.valueOf(i)).start();
        }
        // 等待计数器归零,然后向下执行
        //countDownLatch.await();

        System.out.println("close door");
    }
}

结果:

close door
4go out
3go out
0go out
2go out
5go out
1go out

将//countDownLatch.await();放开,结果(里面的出门顺序我们并不关心):

3go out
0go out
1go out
4go out
5go out
2go out
close door

原理:
每次有线程调用countdown()数量-1;当计数器变为0时,countDownLatch.await()就会被唤醒,继续执行。

假如我们创建了5个线程,导致计算器一直无法为0,程序就无法继续向下执行。

1.3 示例二:

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(3);
    CountDownLatch latch = new CountDownLatch(3);
    for (int i = 0; i < 3; i++) {
        int k = i;
        service.submit(() -> {
            System.out.println("in:" + k);
            latch.countDown();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("out:" + k + "===" + System.currentTimeMillis());
        });
    }
    Thread.sleep(2000);
    latch.await();
    System.out.println("\n欢迎来到召唤师峡谷:" + System.currentTimeMillis());
    service.shutdown();
}

结果:

in:0
in:2
in:1
out:2===1680862433699
out:0===1680862433699
out:1===1680862433699

欢迎来到召唤师峡谷:1680862434711

根据打印时间可以看出,当线程调用countDown方法后,会继续往下执行

1.4 示例三:

public class TestCountDownLatch2 extends Thread {
    private CountDownLatch cdl;
    private int sleepSecond;

    public TestCountDownLatch2(String name, CountDownLatch cdl, int sleepSecond) {
        super(name);
        this.cdl = cdl;
        this.sleepSecond = sleepSecond;
    }

    public void run() {
        try {
            System.out.println(this.getName() + "启动了,时间为" + System.currentTimeMillis());
            Thread.sleep(sleepSecond * 1000);
            cdl.countDown();
            System.out.println(this.getName() + "执行完了,时间为" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        CountDownLatch cdl = new CountDownLatch(3);
        DoneThread dt0 = new DoneThread("DoneThread1", cdl);
        DoneThread dt1 = new DoneThread("DoneThread2", cdl);
        dt0.start();
        dt1.start();
        TestCountDownLatch2 wt0 = new TestCountDownLatch2("WorkThread1", cdl, 2);
        TestCountDownLatch2 wt1 = new TestCountDownLatch2("WorkThread2", cdl, 3);
        TestCountDownLatch2 wt2 = new TestCountDownLatch2("WorkThread3", cdl, 4);
        wt0.start();
        wt1.start();
        wt2.start();
    }
}

class DoneThread extends Thread {
    private CountDownLatch cdl;

    public DoneThread(String name, CountDownLatch cdl) {
        super(name);
        this.cdl = cdl;
    }

    public void run() {
        try {
            System.out.println(this.getName() + "要等待了, 时间为" + System.currentTimeMillis());
            cdl.await();
            System.out.println(this.getName() + "等待完了, 时间为" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果:

WorkThread1启动了,时间为1594919000447
WorkThread2启动了,时间为1594919000447
DoneThread1要等待了, 时间为1594919000447
DoneThread2要等待了, 时间为1594919000447
WorkThread3启动了,时间为1594919000449
WorkThread1执行完了,时间为1594919002476
WorkThread2执行完了,时间为1594919003477
WorkThread3执行完了,时间为1594919004481
DoneThread2等待完了, 时间为1594919004481
DoneThread1等待完了, 时间为1594919004481

分析:

  1. 启动2个线程DoneThread线程等待3个WorkThread全部执行完
  2. 3个WorkThread全部执行完,最后执行完的WorkThread3执行了秒符合预期
  3. 后三句从时间上看几乎同时出现,说明CountDownLatch设置为3,WorkThread3执行完,两个wait的线程马上就执行后面的代码了

这相当于是一种进化版本的等待/通知机制,它可以的实现的是多个工作线程完成任务后通知多个等待线程开始工作,之前的都是一个工作线程完成任务通知一个等待线程或者一个工作线程完成任务通知所有等待线程。

CountDownLatch其实是很有用的,特别适合这种将一个问题分割成N个部分的场景,所有子部分完成后,通知别的一个/几个线程开始工作。比如我要统计C、D、E、F盘的文件,可以开4个线程,分别统计C、D、E、F盘的文件,统计完成把文件信息汇总到另一个/几个线程中进行处理

1.5 应用一:模拟LOL加载玩家

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(10);
    CountDownLatch latch = new CountDownLatch(10);
    String[] all = new String[10];
    for (int i = 0; i < 10; i++) {
        int k = i;
        service.submit(() -> {
            for (int j = 0; j <= 100; j++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                all[k] = j + "%";
                System.out.print("\r" + Arrays.toString(all));
            }
            latch.countDown();
        });
    }
    latch.await();
    System.out.println("\n欢迎来到召唤师峡谷");
    service.shutdown();
}

结果:

// 加载过程中
[93%, 93%, 92%, 92%, 93%, 93%, 92%, 93%, 93%, 93%]

// 加载完成
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
欢迎来到召唤师峡谷

2. CyclicBarrier

CyclicBarri[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,从字面理解是指循环屏障,它可以协同多个线程,让多个线程在这个屏障前等待,直到所有线程都达到了这个屏障时,再一起继续执行后面的动作。

构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行。

跟CountdownLatch一样,但这个可以重用

2.1 CountdownLatch的瓶颈 :

现在有任务1和任务2,希望他们能够执行3次:

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 3; i++) {
        CountDownLatch latch = new CountDownLatch(2);
        service.submit(() -> {
            log.debug("task1 start....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
        });
        service.submit(() -> {
            log.debug("task2 start....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
        });
        latch.await();
        log.debug("finish....");
    }
    service.shutdown();
}

结果:

16:55:03.197 [pool-1-thread-1] DEBUG test1 - task1 start....
16:55:03.198 [pool-1-thread-2] DEBUG test1 - task2 start....
16:55:05.223 [main] DEBUG test1 - finish....
16:55:05.224 [pool-1-thread-3] DEBUG test1 - task1 start....
16:55:05.224 [pool-1-thread-4] DEBUG test1 - task2 start....
16:55:07.238 [main] DEBUG test1 - finish....
16:55:07.240 [pool-1-thread-1] DEBUG test1 - task2 start....
16:55:07.240 [pool-1-thread-5] DEBUG test1 - task1 start....
16:55:09.241 [main] DEBUG test1 - finish....

可以看到任务是执行了3次,但是countdownlatch也被创建了3次,不能够被重用。

2.2 使用CyclicBarrier 解决重用问题:

CyclicBarrier 在await计数等于0之后,如果再次被调用,那么它的计数会被重新初始化为2即第一次初始化时设置的值

public static void main(String[] args) throws InterruptedException {
	// 注意线程数要一致
    ExecutorService service = Executors.newFixedThreadPool(2);
    CyclicBarrier barrier = new CyclicBarrier(2, () -> {
        log.debug("finish....");
    });
    for (int i = 0; i < 3; i++) {
        // CountDownLatch latch = new CountDownLatch(2);
        service.submit(() -> {
            log.debug("task1 start....");
            try {
                Thread.sleep(500);
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
        service.submit(() -> {
            log.debug("task2 start....");
            try {
                Thread.sleep(2000);
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
    }
    service.shutdown();
}
17:02:12.692 [pool-1-thread-1] DEBUG test1 - task1 start....
17:02:12.692 [pool-1-thread-2] DEBUG test1 - task2 start....
17:02:14.712 [pool-1-thread-2] DEBUG test1 - finish....
17:02:14.715 [pool-1-thread-1] DEBUG test1 - task1 start....
17:02:14.715 [pool-1-thread-2] DEBUG test1 - task2 start....
17:02:16.723 [pool-1-thread-2] DEBUG test1 - finish....
17:02:16.724 [pool-1-thread-2] DEBUG test1 - task1 start....
17:02:16.724 [pool-1-thread-1] DEBUG test1 - task2 start....
17:02:18.730 [pool-1-thread-1] DEBUG test1 - finish....

2.3 示例一:

模仿一个召唤神龙的代码:

public class TestCyclicBarrier2 {
    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤成功");
        });

        for (int i = 0; i < 7; i++) {
            // 注意lambda表达式要想获取外面局部变量的值,
            // 也就是匿名内部类要想获取要不变量的值,这个值必须是final的(jdk8以后可以不定义为final)
            // 并且不能修改
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
                try {
                    cyclicBarrier.await(); //等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

当cyclicBarrier 中的线程数量达到7个以后,即当等待的线程数达到7个的时候,执行构造器中的新建的Runnable中的run方法。

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

2.4 示例二:

public class TestCyclicBarrier1 extends Thread {
    private CyclicBarrier cb;
    private int sleepSecond;

    public TestCyclicBarrier1(CyclicBarrier cb, int sleepSecond) {
        this.cb = cb;
        this.sleepSecond = sleepSecond;
    }

    public void run() {
        try {
            System.out.println(this.getName() + "运行了");
            Thread.sleep(sleepSecond * 1000);
            System.out.println(this.getName() + "准备等待了, 时间为" + System.currentTimeMillis());
            cb.await();
            System.out.println(this.getName() + "结束等待了, 时间为" + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(3, () -> {
            System.out.println("CyclicBarrier的所有线程await()结束了,我运行了, 时间为" + System.currentTimeMillis());
        });
        TestCyclicBarrier1 cbt0 = new TestCyclicBarrier1(cb, 3);
        TestCyclicBarrier1 cbt1 = new TestCyclicBarrier1(cb, 6);
        TestCyclicBarrier1 cbt2 = new TestCyclicBarrier1(cb, 9);
        cbt0.start();
        cbt1.start();
        cbt2.start();
    }
}

结果:

Thread-1运行了
Thread-0运行了
Thread-2运行了
Thread-0准备等待了, 时间为1594921353485
Thread-1准备等待了, 时间为1594921356484
Thread-2准备等待了, 时间为1594921359484
CyclicBarrier的所有线程await()结束了,我运行了, 时间为1594921359484
Thread-2结束等待了, 时间为1594921359493
Thread-1结束等待了, 时间为1594921359493
Thread-0结束等待了, 时间为1594921359493

从运行结果看,由于是同一个CyclicBarrier,Thread-0先运行到了await()的地方,等着;Thread-2接着运行到了await()的地方,还等着;Thread-1最后运行到了await()的地方,所有的线程都运行到了await()的地方,所以三个线程以及指定的Runnable"同时"运行后面的代码,可以看到,await()之后,四个线程运行的时间一模一样,都是1444650322313。

注意,因为使用CyclicBarrier的线程都会阻塞在await方法上,所以在线程池中使用CyclicBarrier时要特别小心,如果线程池的线程过少,那么就会发生死锁了

从使用来看,可能有人觉得CyclicBarrier和CountDownLatch有点像,都是多个线程等待相互完成之后,再执行后面的代码。实际上,CountDownLatch和CyclicBarrier都是用于多个线程间的协调的,它们二者的几个差别是:

3. 二者区别:

  1. CountDownLatch是在多个线程都进行了latch.countDown()后才会触发事件,唤醒await()在latch上的线程,而执行countDown()的线程,执行完countDown()后会继续自己线程的工作;CyclicBarrier是一个栅栏,用于同步所有调用await()方法的线程,线程执行了await()方法之后并不会执行之后的代码,而只有当执行await()方法的线程数等于指定的parties之后,这些执行了await()方法的线程才会同时运行
  2. CountDownLatch不能循环使用,计数器减为0就减为0了,不能被重置;CyclicBarrier提供了reset()方法,支持循环使用
  3. CountDownLatch当调用countDown()方法的线程数等于指定的数量之后,可以唤起多条线程的任务;CyclicBarrier当执行await()方法的线程等于指定的数量之后,只能唤起一个BarrierAction
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值