Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier

1,CountDownLatch

倒计时器,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。

举了例子:集齐七龙珠,就可以召唤神龙。

下边需要派7个人(7个线程)去分别去找这7颗不同的龙珠,每个人找到之后,还需要等待的龙珠个数减1,那么当全部的人都找到龙珠之后,那么就可以召唤神龙了。

具体实现代码如下:

(不管是啥代码,即使是简单的测试代码,也都希望各位遵守代码规范,方便你我他。)

package com.lxk.thread.countdownlatch;

import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.junit.Test;

import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.*;

/**
 * CountDownLatch 测试
 *
 * @author lxk on 2018/4/17
 */
public class CountDownLatchTest {
    private static final Integer THREAD_COUNT_NUM = 7;
    private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT_NUM);

    @Test
    public void main() throws InterruptedException {
        ExecutorService executor = initExecutor();

        List list = Collections.synchronizedList(Lists.newArrayList());
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i + 1;
            executor.execute(() -> findDragonBall(index, list));
        }
        countDownLatch.await();
        System.out.println("集齐七龙珠,召唤神龙!");
        System.out.println(list.toString());
        executor.shutdown();
    }

    /**
     * 初始化线程池
     */
    private ExecutorService initExecutor() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("my-ThreadPool-%d").build();
        return new ThreadPoolExecutor(7, 10, 5, TimeUnit.SECONDS, new SynchronousQueue<>(),
                namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    }

    /**
     * 找第index颗星的龙珠
     *
     * @param index 第几颗星第龙珠
     * @param list  存所有龙珠的list
     */
    private void findDragonBall(int index, List list) {
        try {
            int sleepInt = new Random().nextInt(3000);
            Thread.sleep(sleepInt);
            System.out.println("第" + index + "颗龙珠已经收集到!    sleep " + sleepInt);
            list.add(index + "");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
    }
}

运行结果:

Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier_第1张图片

阿里建议不使用Executors去创建线程池,因为这个隐藏了线程池的实现

使用ThreadPoolExecutor去创建,更加明确线程池的运行规则,规避资源耗尽的风险。

public ThreadPoolExecutor(int corePoolSize,
                      int maximumPoolSize,
                      long keepAliveTime,
                      TimeUnit unit,
                      BlockingQueue workQueue,
                      ThreadFactory threadFactory,
                      RejectedExecutionHandler handler)

从上图的运行结果可以看到,这7个龙珠,在找到之前,都sleep了个随机int时间,并不是按照顺序找到的。最终都找到了。

(可以考虑下,先打印找到,再sleep,还是先sleep,再打印找到的运行差别。)

(1)CountDownLatch的构造函数,7表示需要等待执行完毕的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

(2)在每一个线程执行完毕之后,都需要执行countDownLatch.countDown()方法,不然计数器就不会准确;

(3)只有所有的线程执行完毕之后,才会执行 countDownLatch.await()之后的代码;

(4)可以看出上述代码中CountDownLatch 阻塞的是主线程;

  (5)   与CountDownLatch的第一次交互是主线程等待其他线程,即await方法的调用。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他n个线程完成各自的任务。

  (6)   其他n个线程必须引用闭锁对象(即代码实例中的countDownLatch变量),因为各个线程需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

2,CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有人都得等着。

还接着上述“集齐七颗龙珠!召唤神龙”的故事。

召唤神龙,需要7个法师去寻找龙珠,但这7个法师并不是一下子就能号召起来的,所以要等待召集齐7个法师,(敲黑板:这是第一个屏障点)然后让他们同时出发,前往不同的地方寻找龙珠,在这七位法师临行时约定找到龙珠之后还回到这个地方等待其他法师找到龙珠。几年之后,第一个法师回来了,然后等待其他的法师。。。,最后所有的法师全部到齐(敲黑板:这是第二个屏障点),然后组队来找我召唤神龙。

下面是代码实现:(没有优化前的代码)

package com.lxk.thread.cyclicbarrier;

import java.util.Random;
import java.util.concurrent.CyclicBarrier;

/**
 * [ok test]
 * 同样的代码,使用main方法去运行,就能正常运行,该sleep就sleep,还都能正常summon 神龙。
 *
 * 可循环使用(Cyclic)的屏障(Barrier)。
 * 它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
 * 直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
 *
 * @author lxk on 2018/4/19
 */
public class CyclicBarrierMainTest {
    private static final Integer THREAD_COUNT_NUM = 7;

    public static void main(String[] args) {
        CyclicBarrier callMasterBarrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
            @Override
            public void run() {
                System.out.println("7个法师召集完毕,同时出发,去往不同的地方,寻找七龙珠。");
                summonDragon();
            }
        });
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i;
            new Thread(() -> {
                try {
                    int sleepInt = new Random().nextInt(3000);
                    Thread.sleep(sleepInt);
                    System.out.println("召集第" + index + "个法师 sleep " + sleepInt);
                    callMasterBarrier.await();
                } catch (Exception e) {

                }
            }).start();
        }
    }

    private static void summonDragon() {
        CyclicBarrier summonDragonBarrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
            @Override
            public void run() {
                System.out.println("集齐七龙珠,召唤神龙。");
            }
        });
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i;
            new Thread(() -> {
                try {
                    int sleepInt = new Random().nextInt(3000);
                    Thread.sleep(sleepInt);
                    System.out.println("第" + index + "个龙珠已经收集到。 sleep " + sleepInt);
                    summonDragonBarrier.await();
                } catch (Exception e) {

                }
            }).start();
        }
    }
}

运行结果截图:

Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier_第2张图片

这个代码呢,又如下的代码警告。

Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier_第3张图片

Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier_第4张图片

我是建议遇到编辑器提示的各种警告,咱最好都给整理一下,使得代码看着干净、整洁,净化观众的眼睛。

下面是优化(净化)后的代码。

package com.lxk.thread.cyclicbarrier;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.Random;
import java.util.concurrent.*;

/**
 * [ok test]
 * CyclicBarrier test 使用 main 方法来测试
 *
 * @author LiXuekai on 2019/9/30
 */
public class CyclicBarrierMain2Test {
    private static final Integer THREAD_COUNT_NUM = 7;
    private static ExecutorService executor = initExecutor();


    public static void main(String[] args) {
        CyclicBarrier callMasterBarrier = new CyclicBarrier(THREAD_COUNT_NUM, CyclicBarrierMain2Test::goAndFindAllDragonBall);
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i + 1;
            executor.execute(() -> callTheMasterTogether(index, callMasterBarrier));
        }
    }

    /**
     * 召集法师
     *
     * @param index             第n个法师
     * @param callMasterBarrier 召集法师的屏障
     */
    private static void callTheMasterTogether(int index, CyclicBarrier callMasterBarrier) {
        try {
            int sleepInt = new Random().nextInt(3000);
            Thread.sleep(sleepInt);
            System.out.println("召集第" + index + "个法师  sleep " + sleepInt);
            callMasterBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 人齐了,就出发找龙珠去
     */
    private static void goAndFindAllDragonBall() {
        System.out.println("7个法师召集完毕,同时出发,去往不同的地方,寻找七龙珠。");
        findAndSummonDragon();
    }

    /**
     * 集龙珠and召神龙
     */
    private static void findAndSummonDragon() {
        CyclicBarrier summonDragonBarrier = new CyclicBarrier(THREAD_COUNT_NUM, CyclicBarrierMain2Test::summonDragon);
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i + 1;
            executor.execute(() -> findDragonBall(index, summonDragonBarrier));
        }
    }

    /**
     * 召唤神龙
     * summon 召唤的意思
     */
    private static void summonDragon() {
        System.out.println("集齐七龙珠,召唤神龙。");
        executor.shutdown();
    }

    /**
     * 找龙珠
     *
     * @param index               第n个龙珠
     * @param summonDragonBarrier 找龙珠的屏障
     */
    private static void findDragonBall(int index, CyclicBarrier summonDragonBarrier) {
        try {
            int sleepInt = new Random().nextInt(3000);
            Thread.sleep(sleepInt);
            System.out.println("第" + index + "个龙珠已经收集到。 sleep " + sleepInt);
            summonDragonBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化线程池
     */
    private static ExecutorService initExecutor() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("my-ThreadPool-%d").build();
        return new ThreadPoolExecutor(14, 20, 5, TimeUnit.MINUTES, new SynchronousQueue<>(),
                namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    }
}

运行结果截图:

Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier_第5张图片

遇到的问题:

在做这个测试的时候,使用@test注解,junit单元测试的时候,就发现,这个第二个屏障在执行的时候,就会出现问题。但是,使用main方法的时候,却能正常执行。

解释不了。。。

下面粘贴一下junit的单元测试代码。

package com.lxk.thread.cyclicbarrier;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.junit.Test;

import java.util.concurrent.*;

/**
 * [error test]
 * 这个使用 junit测试,就会出现有时候没有找到7个龙珠,召唤神龙。。。。。。。。
 * 可循环使用(Cyclic)的屏障(Barrier)。
 * 它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
 * 直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
 *
 * @author lxk on 2018/4/19
 */
public class CyclicBarrierJunitTest {
    private static final Integer THREAD_COUNT_NUM = 7;
    private static ExecutorService executor = initExecutor();

    @Test
    public void main() {
        CyclicBarrier callMasterBarrier = new CyclicBarrier(THREAD_COUNT_NUM, this::goAndFindAllDragonBall);
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i + 1;
            executor.execute(() -> callTheMasterTogether(index, callMasterBarrier));
        }
    }

    /**
     * 召集法师
     *
     * @param index             第n个法师
     * @param callMasterBarrier 召集法师的屏障
     */
    private void callTheMasterTogether(int index, CyclicBarrier callMasterBarrier) {
        try {
            System.out.println("召集第" + index + "个法师");
            callMasterBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 人齐了,就出发找龙珠去
     */
    private void goAndFindAllDragonBall() {
        System.out.println("7个法师召集完毕,同时出发,去往不同的地方,寻找七龙珠。");
        findAndSummonDragon();
    }

    /**
     * 集龙珠and召神龙
     */
    private void findAndSummonDragon() {
        CyclicBarrier summonDragonBarrier = new CyclicBarrier(THREAD_COUNT_NUM, this::summonDragon);
        for (int i = 0; i < THREAD_COUNT_NUM; i++) {
            int index = i + 1;
            executor.execute(() -> findDragonBall(index, summonDragonBarrier));
        }
    }

    /**
     * 召唤神龙
     * summon 召唤的意思
     */
    private void summonDragon() {
        System.out.println("集齐七龙珠,召唤神龙。");
        executor.shutdown();
    }

    /**
     * 找龙珠
     *
     * @param index               第n个龙珠
     * @param summonDragonBarrier 找龙珠的屏障
     */
    private void findDragonBall(int index, CyclicBarrier summonDragonBarrier) {
        System.out.println("第" + index + "个龙珠已经收集到。");
        try {
            summonDragonBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化线程池
     */
    private static ExecutorService initExecutor() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("my-ThreadPool-%d").build();
        return new ThreadPoolExecutor(14, 20, 5, TimeUnit.MINUTES, new SynchronousQueue<>(),
                namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    }
}

这个代码在运行的时候,就会出现下面的情况

Java 线程计数器 CountDownLatch 和 循环屏障 CyclicBarrier_第6张图片

有兴趣的老铁可以亲自测试一下,多运行几次就会出现这个结果;

路过的能解释这个现象的大神,给解释一下原因呗。

 

代码中设置了两个屏障点,第一个用于召集7个法师,等7个法师召集完后,再设置另一个屏障点,7位法师去寻找龙珠,然后召唤神龙,中间有个嵌套的关系!

 

CyclicBarrier和CountDownLatch的区别

(1)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

(2)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。

(3)CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。

你可能感兴趣的:(java,多线程)