并发编程(三)实战篇 线程池优化 CountDownLatch线程等待 CyclicBarrier线程同步 CompletionService 并行任务

本文来自我的微信公众号 : https://mp.weixin.qq.com/s/Ldq-GsaAMLbHZ6enhwaB7A

 

系统开发中,可能会有这么一系列的操作,来处理数据的重复或者不对称,流程如下:

while(条件) {

    //查询A

    aList = getAList();

 

    //查询B

    bList = getBList();

 

    //比对A和B

    diff = check(aList, bList);

 

    //处理diff

    insert(diff);

}

因为数据量很大,所以查询getAList()操作和getBList()操作用时很长,要跑完一次所需时间较长,怎么优化?

目前的任务是单线程执行的,且getAList和getBList没有先后顺序依赖,优化方案自然而然就想到了利用多线程来并行处理。

 

用多线程实现并行处理

 

查询操作getAList和getBList的并行处理,能够大大缩短查询总共所需时间。 代码如下:

while(条件) {

    //查询A

    Thread T1 = new Thread(() -> {

     aList = getAList();

    });

    T1.start();

    

    //查询B

    Thread T1 = new Thread(() -> {

     bList = getBList();

    });

    T2.start();

    

    //等待T1和T2执行完

    T1.join();

    T2.join();

    

    //比对A和B

    diff = check(aList, bList);

    //处理diff

    insert(diff);

}

 

每次执行任务,都需要创建线程,而创建是耗费时间的,那线程可不可以重复利用呢? 可以,用线程池。

 

用线程池优化和CountDownLatch实现线程等待

 

CountDownLatch主要解决一个线程(主线程)等待多个线程的场景,计算器不能循环利用,下次用的时候要再次new出来。

 

// 创建 2 个线程的线程池

ThreadPoolUtils {

static ExecutorService fixedThreadPool = 

          Executors.newFixedThreadPool(2);

       public static ExecutorService getFixThreadPools(){

            return fixedThreadPool;

        }

}

while(条件) {

    // 计数器初始化为 2

    CountDownLatch latch = new CountDownLatch(2);

    

    //查询A

    ThreadPoolUtils.getFixThreadPools().execute(() -> {

     aList = getAList();

        latch.countDown();

    });

    

    //查询B

    ThreadPoolUtils.getFixThreadPools().execute(() -> {

     bList = getBList();

        latch.countDown();

    });

    

    //等待两个查询执行完

    latch.await();

    

    //比对A和B

    diff = check(aList, bList);

    

    //处理diff

    insert(diff);

}

仔细的你定可以看出, 查询操作 和 diff操作也是可以并行的,比如,查询完,不需要等到diff操作完成,就可以马上再去下一次查询。

这是不是像生产-消费者模型。那么设计2个队列:A队列和B队列,分别保存getAList和getBList的结果。每次A队列出一个元素,B队列出一个元素,

然后对这两个元素执行diff操作。值得注意的是:A队列和B队列都必须出一个元素,才能执行diff操作,那怎么保证同步呢?

 

用CyclicBarrier实现线程同步

 

CyclicBarrier是一组线程之间互相等待,计算器可以循环利用,一旦减到0会自动重置到初始值,且CyclicBarrier可以设置callback函数。

代码如下:

 

        // A队列

     Vector aList;

     // B队列

     Vector bList;

     // 线程池 

     Executor executor = Executors.newFixedThreadPool(1);

     final CyclicBarrier barrier = new CyclicBarrier(2, ()->{

         executor.execute(()->check());

       });

       

      check(){

       A a = aList.remove(0);

       B b = bList.remove(0);

       // 比对A和B

       diff = check(a, b);

       // 处理diff

       insert(diff);

     }

       

       Thread T1 = new Thread(()->{

         while(条件){

           // 查询A

          aList.add(getAList());

           // 等待

           barrier.await();

         }

       });

       T1.start();  

 

       Thread T2 = new Thread(()->{

         while(条件){

           // 查询B

          bList.add(getBList());

           // 等待

           barrier.await();

         }

       });

       T2.start();

 

以上对这段逻辑的优化可以算是告一段落了?

 

不过,优化可以是endless的。

比如,上面是使用CountDownLatch实现了线程间的等待,其实我们也可以用Future接口来实现线程之间的等待。

    

    while(条件) {

    

        //查询A

    Future> aList = ThreadPoolUtils.getFixThreadPools().submit(() -> getAlist());

    

    //查询B

    Future> bList = ThreadPoolUtils.getFixThreadPools().submit(() -> getBlist());

    

    //比对A和B

    diff = check(aList.get(), bList.get());

    

    //处理diff

    insert(diff);

    }

 

或者用FutureTask工具类来实现线程之间的等待

 

FutureTask> aListFTask = new FutureTask<>(() -> getAlist());

ThreadPoolUtils.getFixThreadPools().submit(aListFTask);

List aList = aListFTask.get();

 

Future似乎解决了线程之间的等待问题,但是,它有一定的局限性,比如要实现这个场景【线程A和线程B只要有一个执行完,就执行线程C】。

虽然Future接口提供了方法isDone来检测是否已经结束,但是在复杂场景下不能写出较简洁的并发代码。

所以jdk提供了另一个CompletableFuture工具类,它提供了非常强大的Future的扩展功能,,其方法支持串行关系、并行关系、汇聚关系的多线程实现,

可以帮助我们简化异步编程的复杂性。

 

强大的CompletableFuture

 

下面给出一些代码,更直观一点。

串行关系:

 

    CompletableFuture f = 

      CompletableFuture.supplyAsync(

         () -> "Hello")   

      .thenApply(s -> s + " World")  

      .thenApply(String::toLowerCase);

    

     System.out.println(f.join());

 

输出: hello world

 

 

汇聚关系(OR):

                            CompletableFuture f1 = 

  CompletableFuture.supplyAsync(()->{

    int t = 2;

    try {

Thread.sleep(3000);

} catch (InterruptedException e) {

}

    return String.valueOf(t);

});

 

CompletableFuture f2 = 

  CompletableFuture.supplyAsync(()->{

    int t = 3;

    try {

Thread.sleep(2000);

} catch (InterruptedException e) {

}

    return String.valueOf(t);

});

 

CompletableFuture f3 = 

  f1.applyToEither(f2,s -> s);

 

System.out.println(f3.join());

 

输出: 3

 

CompletionService

 

future.get()有阻塞,如果有大任务,性能极低,主线程被阻塞,导致后续任务一直等待,可用completionService来解决这种阻塞,可以先执行后续,在反过来看下大任务有没有执行完,避免浪费时间。如果大任务实在太大,即使执行完其他任务,主线程还是要等待。怎么办呢?

可用Fork/Join,一个并行计算的框架,支持分治任务模型,Fork对应任务分解,Join对应结果合并。相当于单机版的MapReduce

 

所以, CompletionService实现了任务先完成可优先获取。

 

给出一段示例代码(和CompletableFuture实现汇聚关系有点类似):

 

ExecutorService executor = Executors.newFixedThreadPool(2);

// 创建 CompletionService

CompletionService cs = new ExecutorCompletionService<>(executor);

// 用于保存 Future 对象

List> futures = new ArrayList<>(2);

// 提交异步任务,并保存 future 到 futures

futures.add(cs.submit(() -> getAList()));

futures.add(cs.submit(() -> getBList()));

// 获取最快返回的任务执行结果

Integer r = 0;

try {

// 只要有一个成功返回,则 break

for (int i = 0; i < 2; ++i) {

r = cs.take().get();

// 简单地通过判空来检查是否成功返回

if (r != null) {

break;

}

}

} finally {

// 取消所有任务

for (Future f : futures)

f.cancel(true);

}

// 返回结果

return r;

 

总结: 对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;而批量的并行任务,则可以通过 CompletionService 来解决;如果并行任务中有大任务,则可以通过“Fork/Join”分治任务来解决。

你可能感兴趣的:(JAVA,并发编程)