并发工具类 CountDownLatch、CyclicBarrier、Semaphore 的用法

android 开发,我们会遇到一些并发场景,虽然不多,但还是有的。我们可以自己写代码解决并发问题,也可以借助java提供的工具类。接下来就介绍几个类及它们的使用场景。

CountDownLatch 是 java 1.5 时被引入的,它的作用是使一个线程等其他线程执行完毕后再执行,这么介绍听起来比较抽象,那举个具体的栗子:在android开发中,某个界面显示数据需要请求接口,但此时不是一个接口,而是两个,也就是说我们需要把两个接口返回的数据处理后再一起显示到UI界面上。可能会问:服务端为什么不把两个接口的数据放到一个接口里?理由有n个,比如一个接口是php,一个是java;比如两个接口牵涉的数据库不是一个部门的;再比如服务端哥们比较懒,为了方便...... 我们自己处理两个接口,一般是每个接口都加一个boolean值,在刷新UI前判断这两个boolean值是否都为true,满足条件后再刷新。此时我们就可以使用 CountDownLatch 来实现这个功能,举个简单的例子,两个子线程执行完后再执行主线程的操作。


    private final static String TAG = "TestActivity";
    
    private void testCountDownLatch() {

        final CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(){
            public void run() {
                try {
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread(){
            public void run() {
                try {
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        try {
            t1.start();
            t2.start();
            Log.e(TAG, "等待2个子线程执行完毕...");
            latch.await();
            Log.e(TAG, "2个子线程已经执行完毕");
            Log.e(TAG, "继续执行主线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


打印数据为:

17:17:03.457 18161-18161/TestActivity: 等待2个子线程执行完毕...
17:17:03.457 18161-18456/TestActivity: 子线程Thread-4正在执行
17:17:03.458 18161-18457/TestActivity: 子线程Thread-5正在执行

17:17:06.458 18161-18456/TestActivity: 子线程Thread-4执行完毕
17:17:06.459 18161-18457/TestActivity: 子线程Thread-5执行完毕
17:17:06.460 18161-18161/TestActivity: 2个子线程已经执行完毕
17:17:06.460 18161-18161/TestActivity: 继续执行主线程

从打印的日志也能看出来,两个子线程执行完后才执行UI线程,这个和上面举的例子一样。需要注意的是构造方法 new CountDownLatch(2) 限制是2,await() 方法是开启等待状态,countDown() 是标识当前线程执行完毕,可以放行了。


CyclicBarrier 用于多个线程计算数据,最后计算数据结果,它与 CountDownLatch 有点像,但 CountDownLatch 是一次性的,CyclicBarrier 是可以循环使用的,就像上面的例子,用 CyclicBarrier 也能实现。CyclicBarrier 的构造方法提供了一个形参 Runnable,当里面的子线程执行完了,就会执行当前 Runnable。

    private void testCyclicBarrier() {
        int N = 2;
        Runnable runable = new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, "当前线程" + Thread.currentThread().getName() + "   所有线程写入完毕,继续处理其他任务...");
            }
        };
        final CyclicBarrier barrier = new CyclicBarrier(N, runable);
        Thread t1 = new Thread(){
            public void run() {
                try {
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"执行完毕");
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread(){
            public void run() {
                try {
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"执行完毕");
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        try {
            t1.start();
            t2.start();
            Log.e(TAG, "等待2个子线程执行完毕...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在子线程中调用 testCyclicBarrier() 方法,打印日志为:

17:53:15.159 27735-27829/TestActivity: 等待2个子线程执行完毕...
17:53:15.160 27735-27831/TestActivity: 子线程Thread-3正在执行
17:53:15.160 27735-27830/TestActivity: 子线程Thread-2正在执行

17:53:18.160 27735-27831/TestActivity: 子线程Thread-3执行完毕
17:53:18.161 27735-27830/TestActivity: 子线程Thread-2执行完毕
17:53:18.161 27735-27830/TestActivity: 当前线程Thread-2   所有线程写入完毕,继续处理其他任务...

看到日志就可以发现,CountDownLatch 是在UI线程中执行,CyclicBarrier 则是在最后一个执行完毕的子线程中执行接下来的操作,如果我们要刷新UI,需要切换到UI线程。testCyclicBarrier() 中,我们可以在方法的末尾,把上面的代码赋值一遍,继续执行,这就是所谓的可循环利用。

 
 Semaphore 的作用,更像是个管理器,举个栗子,本人农村长大,村头一片土地要浇地,这是二十户人的地,东西两头各有一水井,我们会先排号,按照排号顺序,一家浇完了另外一家继续浇地。有二十块地要浇,但由于是两口水井,同一时刻最多只能浇两块地。

    private static void testSemaphore(){
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i< 20; i++){
            new IrrigateThread("用户 " + i, semaphore).start();
        }

    }
    static class IrrigateThread extends Thread{
        private String name;
        private Semaphore semaphore;

        public IrrigateThread(String name, Semaphore semaphore){
            this.name = name;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("IrrigateThread:  开始浇地  " +  name +"   " + getNowDate());
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("IrrigateThread:  浇地完毕  " +  name +"   " + getNowDate());
                semaphore.release();
            }
        }
    }

    public static String getNowDate() {
        Date currentTime = new Date();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        String dateString = formatter.format(currentTime);
        return dateString;
    }


    
打印日志为

IrrigateThread:  开始浇地  用户 1   2020-04-16 15:01:25:725
IrrigateThread:  开始浇地  用户 0   2020-04-16 15:01:25:725
IrrigateThread:  浇地完毕  用户 1   2020-04-16 15:01:26:751
IrrigateThread:  开始浇地  用户 2   2020-04-16 15:01:26:752
IrrigateThread:  浇地完毕  用户 0   2020-04-16 15:01:26:751
IrrigateThread:  开始浇地  用户 3   2020-04-16 15:01:26:754
IrrigateThread:  浇地完毕  用户 2   2020-04-16 15:01:27:752
IrrigateThread:  开始浇地  用户 4   2020-04-16 15:01:27:752
IrrigateThread:  浇地完毕  用户 3   2020-04-16 15:01:27:754
IrrigateThread:  开始浇地  用户 5   2020-04-16 15:01:27:754
IrrigateThread:  浇地完毕  用户 4   2020-04-16 15:01:28:752
IrrigateThread:  开始浇地  用户 6   2020-04-16 15:01:28:752
IrrigateThread:  浇地完毕  用户 5   2020-04-16 15:01:28:754
IrrigateThread:  开始浇地  用户 7   2020-04-16 15:01:28:754
IrrigateThread:  浇地完毕  用户 6   2020-04-16 15:01:29:754
IrrigateThread:  开始浇地  用户 8   2020-04-16 15:01:29:755
IrrigateThread:  浇地完毕  用户 7   2020-04-16 15:01:29:757
IrrigateThread:  开始浇地  用户 9   2020-04-16 15:01:29:757
IrrigateThread:  浇地完毕  用户 8   2020-04-16 15:01:30:755
IrrigateThread:  开始浇地  用户 10   2020-04-16 15:01:30:755
IrrigateThread:  浇地完毕  用户 9   2020-04-16 15:01:30:758
IrrigateThread:  开始浇地  用户 11   2020-04-16 15:01:30:758
IrrigateThread:  浇地完毕  用户 10   2020-04-16 15:01:31:755
IrrigateThread:  开始浇地  用户 12   2020-04-16 15:01:31:755
IrrigateThread:  浇地完毕  用户 11   2020-04-16 15:01:31:758
IrrigateThread:  开始浇地  用户 13   2020-04-16 15:01:31:758
IrrigateThread:  浇地完毕  用户 12   2020-04-16 15:01:32:755
IrrigateThread:  开始浇地  用户 14   2020-04-16 15:01:32:755
IrrigateThread:  浇地完毕  用户 13   2020-04-16 15:01:32:759
IrrigateThread:  开始浇地  用户 15   2020-04-16 15:01:32:759
IrrigateThread:  浇地完毕  用户 14   2020-04-16 15:01:33:756
IrrigateThread:  开始浇地  用户 16   2020-04-16 15:01:33:756
IrrigateThread:  浇地完毕  用户 15   2020-04-16 15:01:33:760
IrrigateThread:  开始浇地  用户 17   2020-04-16 15:01:33:760
IrrigateThread:  浇地完毕  用户 16   2020-04-16 15:01:34:756
IrrigateThread:  开始浇地  用户 18   2020-04-16 15:01:34:756
IrrigateThread:  浇地完毕  用户 17   2020-04-16 15:01:34:760
IrrigateThread:  开始浇地  用户 19   2020-04-16 15:01:34:760
IrrigateThread:  浇地完毕  用户 18   2020-04-16 15:01:35:757
IrrigateThread:  浇地完毕  用户 19   2020-04-16 15:01:35:760

我们可以看到,按照顺序,依次执行。


业务中有个场景,比如说某个页面要显示的数据有点特殊,有三个接口,哪个接口先获取到就显示哪个。如果直接这么做了,对手机流量和服务器资源是个浪费,接下来产品同学就定策略了,三个接口排个序,依次执行,间隔1秒。也就是说第一个请求后,如果1秒到了,数据没返回,那么调用第二个,依次类推。简单写个 demo   

 private static void test10A() {

        final Semaphore semaphore = new Semaphore(1);
        final AtomicInteger workComplete = new AtomicInteger(0);
        final AtomicInteger taskCounter = new AtomicInteger(0);
        System.out.println("  test10   first   "  + getNowDate());
        int count = 3;
        while (count > 0){
            try {
                semaphore.tryAcquire(1000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException ex) {
            }
            if (workComplete.get() == 1) {
                return;
            }
            taskCounter.incrementAndGet();
            System.out.println("  test10   second   " + "   " + count-- + "     " + getNowDate());
            final int value = count;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Ru r = new Ru(workComplete, taskCounter, semaphore);
                    try {
                        if(value == 2){
                            Thread.sleep(2500);
                        } else {
                            Thread.sleep(500);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        r.onSuccess();
                    }
                }
            }).start();
        }
        System.out.println("  test10   three   " + getNowDate());
        try {
            semaphore.tryAcquire(1000L, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {

        }
        System.out.println("  test10   three2   " + getNowDate());

        if (workComplete.get() == 0) {
            workComplete.set(1);
            System.out.println("  test10   deliver   " + getNowDate());
        }

    }

    static class Ru  {
         AtomicInteger workComplete;//加载任务是否完成的标志位
         AtomicInteger taskCounter;//任务计数器
         Semaphore semaphore;
        public Ru(AtomicInteger workComplete, AtomicInteger taskCounter, Semaphore semaphore){
            this.workComplete = workComplete;
            this.taskCounter = taskCounter;
            this.semaphore = semaphore;
        }

        public void onFail() {
            if (taskCounter.decrementAndGet() == 0) {
                System.out.println("  test10   onFail   " + getNowDate());
                semaphore.release();
            }
        }

        public void onSuccess(){
            workComplete.set(1);
            onFail();
        }
    }

配合 AtomicInteger 使用,进行拦截。打印日志为

  test10   first   2020-04-16 16:24:03:256
  test10   second      3     2020-04-16 16:24:03:280
  test10   second      2     2020-04-16 16:24:04:282
  test10   onFail   2020-04-16 16:24:05:782

明显是第二条线程的时间短,在1秒内返回了,所以第三条就不用执行了。 tryAcquire(long timeout, TimeUnit unit) 这个方法意思是在当前线程,阻塞timeout 后继续执行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值