JUC-(5)并发工具(上)

java.util.concurrent中给我们提供了很多的并发工具,通过这些并发工具我们能很简单的实现许多强大的功能.这里我主要介绍一下比较常用的一些并发工具的使用场景.

CountDownLatch(闭锁)

简单的来说它可以在初始化时设定一个计数器,这个值只能设定一次后面无法再次修改.同时还提供一个await方法,在计数器的值还未到达0之前调用该方法的线程将一直等待.通过每调用一次countDown将会使该计数值减1,直到计数器的值减到0之后,等待在该对象上的线程才能继续运行.
如果上面的描述没看懂没关系,我们举一个生活中的例子来类比.假如旅行社要搞一个50个人的旅游团(闭锁中的计数器)需要等到名额满了之后才会开始出发(计算器归0).每当有一个人过来报名成功名额就减一(计数器减一),而且报完名的人是可以去干别的事情.当名额未满时旅行社只能等待或者去干别的事(线程调用await等待),直到名额满了旅行社就可以出发了(调用线程继续执行).

例如现在我们有一个数据统计页接口,这个接口里面需要返回来自不同平台的一些统计数据.例如我们要获取淘宝京东拼多多等多个平台的订单数据.按照我们的惯性逻辑来做,可能会查询淘宝平台的数据,然后接着查询京东平台的数据依次类推.这样的查询是可以完成任务,但是接口的响应时间不会很理想.因为这些任务都是串行执行的,最后所花费的时间就是所有的时间之和.我们可以使用多线程配合CountDownLatch让这些查询任务并行运行,大大的提高我们接口的响应时间.

public class App2 {
    public static void main(String[] args) throws Exception {
        //开始时间
        long start = System.currentTimeMillis();
        //创建闭锁
        CountDownLatch cdl = new CountDownLatch(3);
        //查询淘宝任务
        QueryPlatformTask tb = new QueryPlatformTask("tb", cdl);
        //查询京东任务
        QueryPlatformTask jd = new QueryPlatformTask("jd", cdl);
        //查询拼多多任务
        QueryPlatformTask pdd = new QueryPlatformTask("pdd", cdl);

        //创建线程池
        ExecutorService service = Executors.newFixedThreadPool(3);
        //提交任务
        Future f1 = service.submit(tb);
        Future f2 = service.submit(jd);
        Future f3 = service.submit(pdd);
        //等待执行完
        cdl.await();
        //关闭线程池
        service.shutdown();
        //获取结果打印
        System.out.println("f1.get() = " + f1.get());
        System.out.println("f2.get() = " + f2.get());
        System.out.println("f3.get() = " + f3.get());
        long end = System.currentTimeMillis();
        //打印耗时时间
        System.out.println("耗时时间:"+(end-start)/1000+"s");
    }
}

/**
 * 平台查询任务
 */
class QueryPlatformTask implements Callable{
    private String platform;
    private CountDownLatch cdl;

    public QueryPlatformTask(String platform, CountDownLatch cdl) {
        this.platform = platform;
        this.cdl = cdl;
    }

    @Override
    public Integer call() throws Exception {
        try {
            switch (platform){
                case "tb":{
                    return queryTb();
                }
                case "jd":{
                    return queryJD();
                }
                case "pdd":{
                    return queryPdd();
                }
                default:{
                    throw new UnsupportedOperationException("不支持的平台操作");
                }
            }
        }finally {
            //请将计数器减一操作放入finally,避免异常导致该操作失败变成死锁
            cdl.countDown();
        }
    }

    //查询淘宝平台数据
    private Integer queryTb() throws InterruptedException {
        //模拟查询耗时
        Thread.sleep(2000);
        System.out.println("淘宝平台数据查询完成");
        return 100;
    }
    //查询京东平台数据
    private Integer queryJD() throws InterruptedException {
        //模拟查询耗时
        Thread.sleep(3000);
        System.out.println("京东平台数据查询完成");
        return 80;
    }
    //查询拼多多平台数据
    private Integer queryPdd() throws InterruptedException {
        //模拟查询耗时
        Thread.sleep(4000);
        System.out.println("拼多多平台数据查询完成");
        return 50;
    }
}

上面代码简单的实现了查询需求,我们将查询任务封装成Callable类型的任务,且每个任务中包含了CountDownLatch实例用来计数.每当完成一个任务时都会将计数器减1,需要特别注意的是在调用countDown方法时一定要放在finally中.因为如果在执行中出现异常,代码还未执行到countDown时就已经因为异常退了,这将导致所有任务已执行完,但是因为异常的线程没有执行计数器减一的操作,最后导致等待的线程始终无法被唤醒.主线程阻塞在await方法上,直到所有任务都完成计数器归0,主线程继续执行打印结果如下:

淘宝平台数据查询完成
京东平台数据查询完成
拼多多平台数据查询完成
f1.get() = 100
f2.get() = 80
f3.get() = 50
耗时时间:4s

从结果也能看出来,任务由串行运行变成并行运行.最后的耗时时间取决于任务中耗时时间最长的一个.

线程池本身也是提供一个invokeAll方法用来执行批量任务,等任务全部执行完全之后等待的线程再继续执行.这两个方法都能实现同样的功能,但是他们有也有些许不同.不同之处主要在于提供的超时方法上有不同.
invokeAll当到了过期时间之后,对于还在线程池队列中的任务会被取消,而正在执行的方法则会调用interrupt来中断线程执行.但是使用CountDownLatch过期时间到了,阻塞的线程会立即执行,并不会关心任务是否执行完成,也不会去调用interrupt中断线程的执行.下面我们使用两段代码来验证:

  • invokeAll
public class App3 {
    public static void main(String[] args) throws InterruptedException {
        //创建任务
        List> tasks = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            tasks.add(new Callable() {
                @Override
                public Object call() throws Exception {
                    System.out.println(Thread.currentThread().getName()+":开始运行");
                    while (!Thread.currentThread().isInterrupted()) {

                    }
                    System.out.println(Thread.currentThread().getName() + ":线程中断退出");
                    return null;
                }
            });
        }

        //创建线程池 固定一个线程,剩余未执行的任务放入队列
        ExecutorService service = Executors.newFixedThreadPool(1);
        //批量提交任务
        System.out.println(Thread.currentThread().getName()+":提交任务");
        service.invokeAll(tasks,2,TimeUnit.SECONDS);
        System.out.println(Thread.currentThread().getName()+":等待结束");
        //不要调用shutdown shutdown 会干扰结果
        service.shutdown();
    }
}
 
 
  • CountDownLatch
public class App4 {
    public static void main(String[] args) throws InterruptedException {
        int taskNumber = 5;
        final CountDownLatch cdl = new CountDownLatch(taskNumber);
        //创建任务
        List> tasks = new ArrayList<>();
        for (int i = 0; i < taskNumber; i++) {
            tasks.add(new Callable() {
                @Override
                public Object call() throws Exception {
                    try {
                        System.out.println(Thread.currentThread().getName()+":开始运行");
                        while (!Thread.currentThread().isInterrupted()) {

                        }
                        System.out.println(Thread.currentThread().getName() + ":线程中断退出");
                        return null;
                    }finally {
                        cdl.countDown();
                    }
                }
            });
        }

        //创建线程池 固定一个线程,剩余未执行的任务放入队列
        ExecutorService service = Executors.newFixedThreadPool(1);
        //批量提交任务
        System.out.println(Thread.currentThread().getName()+":提交任务");
        for (Callable task : tasks) {
            service.submit(task);
        }
        cdl.await(2,TimeUnit.SECONDS);
        System.out.println(Thread.currentThread().getName()+":等待结束");
        //不要调用shutdown shutdown 会干扰结果
        service.shutdown();
    }
}
 
 
  • 结果
// 结果1
main:提交任务
pool-1-thread-1:开始运行
main:等待结束
pool-1-thread-1:线程中断退出
// 结果2
main:提交任务
pool-1-thread-1:开始运行
main:等待结束

上面的代码中需要注意的的点有两个:创建线程池和线程池shutdwon方法.这两个如果设置的不合理不太好说明invokeAllCountDownLatch之间的区别.如果不懂为什么请看我之前写的关于线程池相关的笔记.
结果1中,在超时时间到了之后,线程池中正在执行的任务会被中断,还在队列中未执行的任务会被取消.最后调用shutdown线程池关闭,主线程执行完毕整个程序结束执行.
结果2中,我们使用countDown设置超时时间.超时时间到了之后主线程推出阻塞继续向下执行,但是可以发现线程池中的线程还在继续执行.因为countDown这种方式它不会去关心线程是不是还在继续执行.而线程池中的线程也无法收到中断响应,这将导致线程池中的任务会继续执行导致无法执行完.最后主线程调用线程池shutdown,不管你等多久程序还是无法终止.这是因为线程池中还有线程在执行,而这部分任务默认都不是守护线程,这将导致程序无法终止.

CyclicBarrier(循环栅栏)

它允许一组线程互相等待,直到所有线程到达某个公共集合点,然后所有的线程再继续执行.这里面强调的执行任务的线程相互等待.同时当所有的任务执行完成之后,Barrier释放所有等待的线程后是可以重用的.
还是拿之前旅行社的例子来做个比喻.现在情况是旅行社团员人数够了,现在准备集合出发了.但是每个游客所在的地方都不一样,每个人到达集合点所用的时间也不一样(每个线程执行完任务到达屏障点的时间不一样).当一个游客到达集合点(线程到达屏障点)需要等待的人就少1(计数器减1),游客到了只能等待其他游客不能干别的事(先到达的线程只能在屏障点等待).直到所有人都已经到了旅行团就可以出发了.
上面的例子使用代码描述为下:

public class App5 {
    public static void main(String[] args) {
        //游客人数
        int touristCount = 5;
        //创建集合点
        CyclicBarrier barrier = new CyclicBarrier(touristCount, new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":人员集合完毕开始出发");
            }
        });
        //创建线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //游客开始出发
        for (int i = 0; i < touristCount; i++) {
            service.execute(new Tourist("游客["+i+"]",(i+1)*1000,barrier));
        }
        //关闭线程池
        service.shutdown();
    }
}

/**
 * 游客
 */
class Tourist implements Runnable{
    //游客的名字
    private String name;
    //赶路花费的时间
    private Integer time;
    private CyclicBarrier barrier;

    public Tourist(String name, Integer time, CyclicBarrier barrier) {
        this.name = name;
        this.time = time;
        this.barrier = barrier;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":"+name+"出发");
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+name+"到达集合点,等待其他游客到达");
        try {
            //当前线程在这个地方等待其他线程到达屏障点
            barrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

最后的打印结果如下:

pool-1-thread-2:游客[1]出发
pool-1-thread-1:游客[0]出发
pool-1-thread-4:游客[3]出发
pool-1-thread-5:游客[4]出发
pool-1-thread-3:游客[2]出发
pool-1-thread-1:游客[0]到达集合点,等待其他游客到达
pool-1-thread-2:游客[1]到达集合点,等待其他游客到达
pool-1-thread-3:游客[2]到达集合点,等待其他游客到达
pool-1-thread-4:游客[3]到达集合点,等待其他游客到达
pool-1-thread-5:游客[4]到达集合点,等待其他游客到达
pool-1-thread-5:人员集合完毕开始出发

创建CyclicBarrier中的barrierAction参数是一个Runnable类型.这个代表当所有的任务执行完成之后才执行这个任务.而且是由最后一个到达屏障点(即完成)的线程来执行.从我们的打印结果可以看出是pool-1-thread-5,因为它是最后一个执行的.

  • CyclicBarrier是可以循环使用的.它提供了一个reset方法用来重置并将它设置回初始状态.但是需要如果在屏障点上已经有线程在上面等待的话,等待的线程将会抛出BrokenBarrierException异常.这将导致还未到达屏障点的其他线程在到达屏障点后一直无限等待.
public class App6 {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"barrierAction");
            }
        });
        //
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName()+":在屏障点等待");
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setName("t1");
        t1.start();
        //
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName()+":在屏障点等待");
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.setName("t2");
        t2.start();

        Thread.sleep(2000L);
        barrier.reset();
    }
}

上面代码执行结果如下:

t1:在屏障点等待
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at com.buydeem.App6$2.run(App6.java:26)
    at java.lang.Thread.run(Thread.java:745)
t2:在屏障点等待

线程t1先到达屏障点,然后过了2s之后main线程调用resetCyclicBarrier实例重置,这直接导致线程t1抛出BrokenBarrierException异常,然后线程t2到达屏障点将在该点上一个值无限等待下去,导致程序无法执行结束.

  • 如果一个线程等待在屏障点,然后将等待中的线程中断.这个操作将会导致等待中的线程抛出InterruptedException异常,这个异常还会传播到其他线程,这将导致其他等待在这个屏障点上的线程会收到BrokenBarrierException异常.这个里面的等待线程包括已经到达等待点的线程和还未到达屏障点的线程,这个操作也将导致屏障点损坏.
public class App7 {
    public static void main(String[] args) throws InterruptedException {
        //创建CyclicBarrier
        CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                System.out.println("barrierAction");
            }
        });
        //创建任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                } catch (BrokenBarrierException e) {
                    System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
                }
            }
        };
        //任务1
        Thread t1 = new Thread(runnable);
        t1.setName("t1");
        t1.start();
        //任务2
        Thread t2 = new Thread(runnable);
        t2.setName("t2");
        t2.start();
        //任务3
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    barrier.await();
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                } catch (BrokenBarrierException e) {
                    System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
                }
            }
        });
        t3.setName("t3");
        t3.start();

        //2s后中断线程1
        Thread.sleep(1000L);
        t1.interrupt();
        //2s打印是否损坏
        Thread.sleep(2000L);
        System.out.println("屏障点是否损坏:"+barrier.isBroken());
    }
}

打印结果如下:

t1:InterruptedException
t2:BrokenBarrierException
t3:BrokenBarrierException
屏障点是否损坏:true

程序运行线程t1t2都到达了阻塞点,然后过了1s之后,线程t1被中断.这个中断导致线程t1t2分别抛出InterruptedException异常和BrokenBarrierException.而线程t3在此之后到达屏障点,也同样抛出BrokenBarrierException异常.最后通过isBroken打印显示屏障点已经被损坏.

  • 如果当前等待在屏障点上的线程超出指定的等待时间,当前线程会抛出TimeoutException异常,而其他线程会抛出BrokenBarrierException异常,同样也会导致屏障点被损坏
public class App8 {
    public static void main(String[] args) throws InterruptedException {
        //创建CyclicBarrier
        CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("barrierAction");
            }
        });
        //任务1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    barrier.await();
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                } catch (BrokenBarrierException e) {
                    System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
                }
            }
        });
        t1.setName("t1");
        t1.start();
        //任务2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    barrier.await(1,TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + ":InterruptedException");
                } catch (BrokenBarrierException e) {
                    System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
                } catch (TimeoutException e) {
                    System.out.println(Thread.currentThread().getName() + ":TimeoutException");
                }
            }
        });
        t2.setName("t2");
        t2.start();

        //2s打印是否损坏
        Thread.sleep(2000L);
        System.out.println("屏障点是否损坏:"+barrier.isBroken());
    }
}
t2:TimeoutException
屏障点是否损坏:true
t1:BrokenBarrierException

线程t1先到达屏障点等待超时时间为1s,而线程t2需要2s才到达屏障点.所以线程t1抛出TimeoutException异常,同时导致线程t2同样抛出BrokenBarrierException异常并且还导致屏障点损坏.

CountDownLatch与CyclicBarrier的差异

网上说他们之间的差异主要在于复用和不复用.其实我不太认同这种说法,我觉得这个并不是他们之间最大的差别.

  • CountDownLatch强调的是主线程等待所有子线程完成任务然后继续执行.而CyclicBarrier强调的是所有的子线程到达一个集合点再继续执行.
  • CountDownLatch中的子线程完成任务后它是可以去干别的事情的,而CyclicBarrier中的子线程只能等待在屏障点是不能去干别的事情的.

以上两点我觉得才是他们之间最大的区别.我用一个例子说明:

  • CountDownLatch
public class App9 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个固定大小为2的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        //创建闭锁
        int count = 5;
        final CountDownLatch cdl = new CountDownLatch(count);
        //创建子任务
        for (int i = 0; i < count; i++) {
            service.execute(new NumberTask(i,cdl));
        }
        //等待任务执行结束
        cdl.await();
        //关闭线程池
        service.shutdown();
    }
}

/**
 * 任务
 */
class NumberTask implements Runnable{
    private Integer id;
    private CountDownLatch cdl;

    public NumberTask(Integer id, CountDownLatch cdl) {
        this.id = id;
        this.cdl = cdl;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+":执行完任务["+id+"]");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            cdl.countDown();
        }
    }
}
  • CyclicBarrier
public class App10 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个固定大小为2的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        //创建闭锁
        int count = 5;
        final CyclicBarrier barrier = new CyclicBarrier(count);
        //创建子任务
        for (int i = 0; i < count; i++) {
            service.execute(new NumberTask2(i,barrier));
        }
        //关闭线程池
        service.shutdown();
    }
}

/**
 * 任务
 */
class NumberTask2 implements Runnable{
    private Integer id;
    private CyclicBarrier barrier;

    public NumberTask2(Integer id, CyclicBarrier barrier) {
        this.id = id;
        this.barrier = barrier;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+":执行完任务["+id+"]");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

打印结果如下:

//结果1
pool-1-thread-1:执行完任务[0]
pool-1-thread-2:执行完任务[1]
pool-1-thread-1:执行完任务[2]
pool-1-thread-2:执行完任务[3]
pool-1-thread-1:执行完任务[4]
//结果2
pool-1-thread-1:执行完任务[0]
pool-1-thread-2:执行完任务[1]

结果1中我们使用的线程池只有2个线程,当子线程完成自己的任务后,会继续去处理其他子任务.而结果2只会打印出两条记录,而且线程不会结束一直阻塞.这是因为在屏障点的线程需要等待其他线程都到达了屏障点才回继续执行,它们只能在屏障点等待.

你可能感兴趣的:(JUC-(5)并发工具(上))