20

闭锁和栅栏的区分以及适用场景

 4 years ago
source link: https://segmentfault.com/a/1190000022941013
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

开篇

相信小伙伴对这个两个词或多或少都有些了解,他们是在并发编程中常用的线程通讯工具。两者十分相似,但是又有不同,导致很多小伙伴也包括我在内产生了很多困惑:他们两个究竟有什么区别,以及适用于什么场景呢?

下面听我缓缓道来,不想看例子或者过程的小伙伴可以拉到最下面看总结呦

闭锁

闭锁(CountDownLatch)坊间俗称计数器,官方( 谷歌机翻,哈哈 )解释:

/**
 * A synchronization aid that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 */

允许一个或多个线程等待,直到在其他线程中执行的一组操作完成的同步辅助程序。

大概意思就是说,可以有一个或者多个线程,等待其他线程都完成某个操作后,再继续执行。

什么意思呢?举个栗子吧:

生活中应该经常遇见一种情况,坐公交车是,尤其是始发站,司机师傅往往为了一次拉更多的乘客,会等到车上乘客的数量到达一定程度以后才会发车。测试代码如下:

public static void main(String[] args) {
        List<Passenger> list = new ArrayList<>();
        Passenger p1 = new Passenger("看会书");
        Passenger p2 = new Passenger("看会手机");
        Passenger p3 = new Passenger("看会风景");
        Passenger p4 = new Passenger("看会售票员");
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 200, 1000, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), new ThreadFactory() {
            private ThreadGroup group = (null == System.getSecurityManager() ? Thread.currentThread().getThreadGroup() : System.getSecurityManager().getThreadGroup());
            private AtomicInteger num = new AtomicInteger();
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(group, r,"zoo" + num.getAndIncrement(),0);
                thread.setDaemon(false);
                return thread;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
        //设定闭锁释放阈值
        CountDownLatch countDownLatch = new CountDownLatch(list.size());
        log.error("司机师傅人够一车再发车,等会人吧...");
        for (Passenger p : list) {
            executor.execute(()->gotoZOO(p,countDownLatch));
        }
        try {
            countDownLatch.await();
            log.error("人够了,起飞!");
            executor.shutdown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    private static void  gotoZOO(Passenger p,CountDownLatch countDownLatch){
        log.error("{}的乘客上车啦",p.getDoWhat());
        try {
            countDownLatch.countDown();
            log.error("{}",p.doWhatOnBus());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class Passenger{
        private String doWhat;

        public Passenger(String doWhat) {
            this.doWhat = doWhat;
        }

        public String getDoWhat() {
            return doWhat;
        }

        public String doWhatOnBus() {
            return "车上好无聊啊,"+doWhat+"吧!";
        }
    }

执行结果

23:46:34.698 [main] ERROR com.test - 司机师傅人够一车再发车,等会人吧...
23:46:34.757 [zoo1] ERROR com.test - 看会手机的乘客上车啦
23:46:34.758 [zoo3] ERROR com.test - 看会售票员的乘客上车啦
23:46:34.757 [zoo0] ERROR com.test - 看会书的乘客上车啦
23:46:34.759 [zoo1] ERROR com.test - 车上好无聊啊,看会手机吧!
23:46:34.759 [zoo3] ERROR com.test - 车上好无聊啊,看会售票员吧!
23:46:34.757 [zoo2] ERROR com.test - 看会风景的乘客上车啦
23:46:34.759 [zoo0] ERROR com.test - 车上好无聊啊,看会书吧!
23:46:34.759 [zoo2] ERROR com.test - 车上好无聊啊,看会风景吧!
23:46:34.759 [main] ERROR com.test - 人够了,起飞!

司机师傅(主线程)要等上了4个乘客以后才发车(等待4个子线程完成完成某件事以后调用countDown方法),而乘客上车(调用countDown)以后该做自己的事还做自己的事情,不会因为上了车就傻呆呆的什么都不干了(不会因为调用了countDown而阻塞自身)。等司机师傅看人够了(到达设定阈值),就发车了。

闭锁总结:

主线程调用await后会阻塞等待其他子线程调用countDown方法将设定阈值减至0,然后在继续执行。

而子线程不会因为调用了countDown方法而阻塞

栅栏

栅栏(CyclicBarrier)官方解释:

/**
 * A synchronization aid that allows a set of threads to all wait for
 * each other to reach a common barrier point.  CyclicBarriers are
 * useful in programs involving a fixed sized party of threads that
 * must occasionally wait for each other. The barrier is called
 * <em>cyclic</em> because it can be re-used after the waiting threads
 * are released.
 */
同步帮助,允许一组线程互相等待,以达到共同的障碍点。 CyclicBarriers在涉及固定大小的线程方的程序中很有用,这些线程有时必须互相等待。该屏障称为<em> cyclic </ em>,因为它可以在释放等待线程后重新使用。

从类注释上我们可以大致了解到,他是运用在一组,也即是多个线程中的,当所有线程到达某个状态前一直阻塞,直到所有线程都达到后再继续执行。而且是可以重复使用的。

上面的描述还是太晦涩了,还是举个栗子:

我们小时候学校都组织过春游,规定好地点,等人到齐了就一起进去玩。写了个简单的例子,看这种场景栅栏是怎么工作的

public static void main(String[] args) {
        List<Boy> list = new ArrayList<>();
        Boy boy1 = new Boy("看老虎");
        Boy boy2 = new Boy("看猩猩");
        Boy boy3 = new Boy("看狮子");
        Boy boy4 = new Boy("看售票员");
        list.add(boy1);
        list.add(boy2);
        list.add(boy3);
        list.add(boy4);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 200, 1000, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), new ThreadFactory() {
            private ThreadGroup group = (null == System.getSecurityManager() ? Thread.currentThread().getThreadGroup() : System.getSecurityManager().getThreadGroup());
            private AtomicInteger num = new AtomicInteger();
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(group, r,"zoo" + num.getAndIncrement(),0);
                thread.setDaemon(false);
                return thread;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化栅栏,设置障碍点阈值
        CyclicBarrier cyclicBarrier = new CyclicBarrier(list.size());
        for (Boy boy : list) {
            executor.execute(()->gotoZOO(boy,cyclicBarrier));
        }
    }

    private static void  gotoZOO(Boy boy,CyclicBarrier cyclicBarrier){
        log.error("人还没到齐呢,等一下吧,{}的小男孩开始等待",boy.getWhere());
        try {
            cyclicBarrier.await();
            log.error("{}",boy.goWhere());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class Boy{
        private String where;
        public Boy(String where) {
            this.where = where;
        }

        public String getWhere() {
            return where;
        }

        public String goWhere() {
            return "人到齐了,我要去"+where+"啦!";
        }
    }

执行结果:

22:05:59.476 [zoo2] ERROR com.test - 人还没到齐呢,等一下吧,看狮子的小男孩开始等待
22:05:59.477 [zoo1] ERROR com.test - 人还没到齐呢,等一下吧,看猩猩的小男孩开始等待
22:05:59.477 [zoo0] ERROR com.test - 人还没到齐呢,等一下吧,看老虎的小男孩开始等待
22:05:59.476 [zoo3] ERROR com.test - 人还没到齐呢,等一下吧,看售票员的小男孩开始等待
22:05:59.484 [zoo0] ERROR com.test - 人到齐了,我要去看老虎啦!
22:05:59.484 [zoo2] ERROR com.test - 人到齐了,我要去看狮子啦!
22:05:59.484 [zoo3] ERROR com.test - 人到齐了,我要去看售票员啦!
22:05:59.484 [zoo1] ERROR com.test - 人到齐了,我要去看猩猩啦!

我们可以发现前三个小男孩在到达以后都没有进到动物园里,而是直到第四个小男孩来到以后,四个小男孩才进入动物园,在此之前每来一个小朋友就多一个小朋友等待(每个线程调用await方法),直到等待所有人到齐(线程阻塞等待达到栅栏障碍点4),各个小男孩再去继续进入动物园看动物(各线程继续执行自己的任务)。就像是动物园大门的栅栏,买的是团体票,每次必须人到齐才放开让小朋友进去一样。

栅栏总结

各子线程相互等待,直到达到栅栏初始化时的阈值,则继续执行

区分以及个人理解

闭锁:有点类似于一个统计功能(可能这也是为什么他俗称计数器),主线程调用await方法阻塞等待统计结果,而子线程只负责在达到统计要求时调用countDown方法告诉主线程我好了,而不会阻塞本身;有一个负责接收结果(主线程)和一个或多个发送数量的(子线程);

栅栏:首先在线程调用await方法时会阻塞当前线程,其次个人理解他没有类似像闭锁那样的主子的关系,他是各个线程相互等待,都到达某个点的时候,则继续执行。

适用场景

其实从上面的区分就能看出一些:如果是需要将多线程执行完成与否的接口汇总到某一个线程中,然后再继续执行的情况,比如每条线程计算一个指标,都计算完成以后再计算所有指标的总和或者其他的,就可以使用闭锁;

而如果只是各个线程需要等各个线程都完成了,再继续自己的事,可以使用栅栏,比如ABC三个线程分别去获取123三个指标,然后再A要取这三个数的平均数,B要取总和,C要取方差,那就需要等ABC都先取完了123这三个指标,才能计算,这时候就可以用到栅栏了。

总结

这两种都是非常好的线程通讯工具,不过细节还是有所差异。

总得来说就是:

闭锁是为了在 某一条线程等待 获取到 其他线程 的执行结果;

而栅栏则是线程间的 相互等待 ,然后再同时开始做 各自的事情

最后

文中的代码只是为了比较好的说明两种工具的差异,写的不好还请小伙伴们多多包涵,如果发现有哪点写的不对的也欢迎大家伙们留言,我们共同进步!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK