Java高并发专题之20、JUC中的Executor框架详解2

本文内容

  1. ExecutorCompletionService出现的背景
  2. 介绍CompletionService接口及常用的方法
  3. 介绍ExecutorCompletionService类及其原理
  4. 示例:执行一批任务,然后消费执行结果
  5. 示例【2种方式】:异步执行一批任务,有一个完成立即返回,其他取消

需要解决的问题

还是举个例子说明更好理解一些。

买新房了,然后在网上下单买冰箱、洗衣机,电器商家不同,所以送货耗时不一样,然后等他们送货,快递只愿送到楼下,然后我们自己将其搬到楼上的家中。

用程序来模拟上面的实现。示例代码如下:

  1. package com.itsoku.chat18;
  2. import java.util.concurrent.*;
  3. /**
  4. * 跟着阿里p7学并发,微信公众号:javacode2018
  5. */
  6. public class Demo12 {
  7. static class GoodsModel {
  8. //商品名称
  9. String name;
  10. //购物开始时间
  11. long startime;
  12. //送到的时间
  13. long endtime;
  14. public GoodsModel(String name, long startime, long endtime) {
  15. this.name = name;
  16. this.startime = startime;
  17. this.endtime = endtime;
  18. }
  19. @Override
  20. public String toString() {
  21. return name + ",下单时间[" + this.startime + "," + endtime + "],耗时:" + (this.endtime - this.startime);
  22. }
  23. }
  24. /**
  25. * 将商品搬上楼
  26. *
  27. * @param goodsModel
  28. * @throws InterruptedException
  29. */
  30. static void moveUp(GoodsModel goodsModel) throws InterruptedException {
  31. //休眠5秒,模拟搬上楼耗时
  32. TimeUnit.SECONDS.sleep(5);
  33. System.out.println("将商品搬上楼,商品信息:" + goodsModel);
  34. }
  35. /**
  36. * 模拟下单
  37. *
  38. * @param name 商品名称
  39. * @param costTime 耗时
  40. * @return
  41. */
  42. static Callable<GoodsModel> buyGoods(String name, long costTime) {
  43. return () -> {
  44. long startTime = System.currentTimeMillis();
  45. System.out.println(startTime + "购买" + name + "下单!");
  46. //模拟送货耗时
  47. try {
  48. TimeUnit.SECONDS.sleep(costTime);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. long endTime = System.currentTimeMillis();
  53. System.out.println(startTime + name + "送到了!");
  54. return new GoodsModel(name, startTime, endTime);
  55. };
  56. }
  57. public static void main(String[] args) throws InterruptedException, ExecutionException {
  58. long st = System.currentTimeMillis();
  59. System.out.println(st + "开始购物!");
  60. //创建一个线程池,用来异步下单
  61. ExecutorService executor = Executors.newFixedThreadPool(5);
  62. //异步下单购买冰箱
  63. Future<GoodsModel> bxFuture = executor.submit(buyGoods("冰箱", 5));
  64. //异步下单购买洗衣机
  65. Future<GoodsModel> xyjFuture = executor.submit(buyGoods("洗衣机", 2));
  66. //关闭线程池
  67. executor.shutdown();
  68. //等待冰箱送到
  69. GoodsModel bxGoodModel = bxFuture.get();
  70. //将冰箱搬上楼
  71. moveUp(bxGoodModel);
  72. //等待洗衣机送到
  73. GoodsModel xyjGooldModel = xyjFuture.get();
  74. //将洗衣机搬上楼
  75. moveUp(xyjGooldModel);
  76. long et = System.currentTimeMillis();
  77. System.out.println(et + "货物已送到家里咯,哈哈哈!");
  78. System.out.println("总耗时:" + (et - st));
  79. }
  80. }

输出:

  1. 1564653121515开始购物!
  2. 1564653121588购买冰箱下单!
  3. 1564653121588购买洗衣机下单!
  4. 1564653121588洗衣机送到了!
  5. 1564653121588冰箱送到了!
  6. 将商品搬上楼,商品信息:冰箱,下单时间[1564653121588,1564653126590],耗时:5002
  7. 将商品搬上楼,商品信息:洗衣机,下单时间[1564653121588,1564653123590],耗时:2002
  8. 1564653136591货物已送到家里咯,哈哈哈!
  9. 总耗时:15076

从输出中我们可以看出几个时间:

  1. 购买冰箱耗时5秒
  2. 购买洗衣机耗时2秒
  3. 将冰箱送上楼耗时5秒
  4. 将洗衣机送上楼耗时5秒
  5. 共计耗时15秒

购买洗衣机、冰箱都是异步执行的,我们先把冰箱送上楼了,然后再把冰箱送上楼了。上面大家应该发现了一个问题,洗衣机先到的,洗衣机到了,我们并没有去把洗衣机送上楼,而是在等待冰箱到货(bxFuture.get();),然后将冰箱送上楼,中间导致浪费了3秒,现实中应该是这样的,先到的先送上楼,修改一下代码,洗衣机先到的,先送洗衣机上楼,代码如下:

  1. package com.itsoku.chat18;
  2. import java.util.concurrent.*;
  3. /**
  4. * 跟着阿里p7学并发,微信公众号:javacode2018
  5. */
  6. public class Demo13 {
  7. static class GoodsModel {
  8. //商品名称
  9. String name;
  10. //购物开始时间
  11. long startime;
  12. //送到的时间
  13. long endtime;
  14. public GoodsModel(String name, long startime, long endtime) {
  15. this.name = name;
  16. this.startime = startime;
  17. this.endtime = endtime;
  18. }
  19. @Override
  20. public String toString() {
  21. return name + ",下单时间[" + this.startime + "," + endtime + "],耗时:" + (this.endtime - this.startime);
  22. }
  23. }
  24. /**
  25. * 将商品搬上楼
  26. *
  27. * @param goodsModel
  28. * @throws InterruptedException
  29. */
  30. static void moveUp(GoodsModel goodsModel) throws InterruptedException {
  31. //休眠5秒,模拟搬上楼耗时
  32. TimeUnit.SECONDS.sleep(5);
  33. System.out.println("将商品搬上楼,商品信息:" + goodsModel);
  34. }
  35. /**
  36. * 模拟下单
  37. *
  38. * @param name 商品名称
  39. * @param costTime 耗时
  40. * @return
  41. */
  42. static Callable<GoodsModel> buyGoods(String name, long costTime) {
  43. return () -> {
  44. long startTime = System.currentTimeMillis();
  45. System.out.println(startTime + "购买" + name + "下单!");
  46. //模拟送货耗时
  47. try {
  48. TimeUnit.SECONDS.sleep(costTime);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. long endTime = System.currentTimeMillis();
  53. System.out.println(endTime + name + "送到了!");
  54. return new GoodsModel(name, startTime, endTime);
  55. };
  56. }
  57. public static void main(String[] args) throws InterruptedException, ExecutionException {
  58. long st = System.currentTimeMillis();
  59. System.out.println(st + "开始购物!");
  60. //创建一个线程池,用来异步下单
  61. ExecutorService executor = Executors.newFixedThreadPool(5);
  62. //异步下单购买冰箱
  63. Future<GoodsModel> bxFuture = executor.submit(buyGoods("冰箱", 5));
  64. //异步下单购买洗衣机
  65. Future<GoodsModel> xyjFuture = executor.submit(buyGoods("洗衣机", 2));
  66. //关闭线程池
  67. executor.shutdown();
  68. //等待洗衣机送到
  69. GoodsModel xyjGooldModel = xyjFuture.get();
  70. //将洗衣机搬上楼
  71. moveUp(xyjGooldModel);
  72. //等待冰箱送到
  73. GoodsModel bxGoodModel = bxFuture.get();
  74. //将冰箱搬上楼
  75. moveUp(bxGoodModel);
  76. long et = System.currentTimeMillis();
  77. System.out.println(et + "货物已送到家里咯,哈哈哈!");
  78. System.out.println("总耗时:" + (et - st));
  79. }
  80. }

输出:

  1. 1564653153393开始购物!
  2. 1564653153466购买洗衣机下单!
  3. 1564653153466购买冰箱下单!
  4. 1564653155467洗衣机送到了!
  5. 1564653158467冰箱送到了!
  6. 将商品搬上楼,商品信息:洗衣机,下单时间[1564653153466,1564653155467],耗时:2001
  7. 将商品搬上楼,商品信息:冰箱,下单时间[1564653153466,1564653158467],耗时:5001
  8. 1564653165469货物已送到家里咯,哈哈哈!
  9. 总耗时:12076

耗时12秒,比第一种少了3秒。

问题来了,上面是我们通过调整代码达到了最优效果,实际上,购买冰箱和洗衣机具体哪个耗时时间长我们是不知道的,怎么办呢,有没有什么解决办法?

CompletionService接口

CompletionService相当于一个执行任务的服务,通过submit丢任务给这个服务,服务内部去执行任务,可以通过服务提供的一些方法获取服务中已经完成的任务。

接口内的几个方法:

  1. Future<V> submit(Callable<V> task);

用于向服务中提交有返回结果的任务,并返回Future对象

  1. Future<V> submit(Runnable task, V result);

用户向服务中提交有返回值的任务去执行,并返回Future对象

  1. Future<V> take() throws InterruptedException;

从服务中返回并移除一个已经完成的任务,如果获取不到,会一致阻塞到有返回值为止。此方法会响应线程中断。

  1. Future<V> poll();

从服务中返回并移除一个已经完成的任务,如果内部没有已经完成的任务,则返回空,此方法会立即响应。

  1. Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;

尝试在指定的时间内从服务中返回并移除一个已经完成的任务,等待的时间超时还是没有获取到已完成的任务,则返回空。此方法会响应线程中断

通过submit向内部提交任意多个任务,通过take方法可以获取已经执行完成的任务,如果获取不到将等待。

ExecutorCompletionService类

ExecutorCompletionService类是CompletionService接口的具体实现。

说一下其内部原理,ExecutorCompletionService创建的时候会传入一个线程池,调用submit方法传入需要执行的任务,任务由内部的线程池来处理;ExecutorCompletionService内部有个阻塞队列,任意一个任务完成之后,会将任务的执行结果(Future类型)放入阻塞队列中,然后其他线程可以调用它take、poll方法从这个阻塞队列中获取一个已经完成的任务,获取任务返回结果的顺序和任务执行完成的先后顺序一致,所以最先完成的任务会先返回。

关于阻塞队列的知识后面会专门抽几篇来讲,大家可以关注一下后面的文章。

看一下构造方法:

  1. public ExecutorCompletionService(Executor executor) {
  2. if (executor == null)
  3. throw new NullPointerException();
  4. this.executor = executor;
  5. this.aes = (executor instanceof AbstractExecutorService) ?
  6. (AbstractExecutorService) executor : null;
  7. this.completionQueue = new LinkedBlockingQueue<Future<V>>();
  8. }

构造方法需要传入一个Executor对象,这个对象表示任务执行器,所有传入的任务会被这个执行器执行。

completionQueue是用来存储任务结果的阻塞队列,默认用采用的是LinkedBlockingQueue,也支持开发自己设置。通过submit传入需要执行的任务,任务执行完成之后,会放入completionQueue中,有兴趣的可以看一下原码,还是很好理解的。

使用ExecutorCompletionService解决文章开头的问题

代码如下:

  1. package com.itsoku.chat18;
  2. import java.util.concurrent.*;
  3. /**
  4. * 跟着阿里p7学并发,微信公众号:javacode2018
  5. */
  6. public class Demo14 {
  7. static class GoodsModel {
  8. //商品名称
  9. String name;
  10. //购物开始时间
  11. long startime;
  12. //送到的时间
  13. long endtime;
  14. public GoodsModel(String name, long startime, long endtime) {
  15. this.name = name;
  16. this.startime = startime;
  17. this.endtime = endtime;
  18. }
  19. @Override
  20. public String toString() {
  21. return name + ",下单时间[" + this.startime + "," + endtime + "],耗时:" + (this.endtime - this.startime);
  22. }
  23. }
  24. /**
  25. * 将商品搬上楼
  26. *
  27. * @param goodsModel
  28. * @throws InterruptedException
  29. */
  30. static void moveUp(GoodsModel goodsModel) throws InterruptedException {
  31. //休眠5秒,模拟搬上楼耗时
  32. TimeUnit.SECONDS.sleep(5);
  33. System.out.println("将商品搬上楼,商品信息:" + goodsModel);
  34. }
  35. /**
  36. * 模拟下单
  37. *
  38. * @param name 商品名称
  39. * @param costTime 耗时
  40. * @return
  41. */
  42. static Callable<GoodsModel> buyGoods(String name, long costTime) {
  43. return () -> {
  44. long startTime = System.currentTimeMillis();
  45. System.out.println(startTime + "购买" + name + "下单!");
  46. //模拟送货耗时
  47. try {
  48. TimeUnit.SECONDS.sleep(costTime);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. long endTime = System.currentTimeMillis();
  53. System.out.println(endTime + name + "送到了!");
  54. return new GoodsModel(name, startTime, endTime);
  55. };
  56. }
  57. public static void main(String[] args) throws InterruptedException, ExecutionException {
  58. long st = System.currentTimeMillis();
  59. System.out.println(st + "开始购物!");
  60. ExecutorService executor = Executors.newFixedThreadPool(5);
  61. //创建ExecutorCompletionService对象
  62. ExecutorCompletionService<GoodsModel> executorCompletionService = new ExecutorCompletionService<>(executor);
  63. //异步下单购买冰箱
  64. executorCompletionService.submit(buyGoods("冰箱", 5));
  65. //异步下单购买洗衣机
  66. executorCompletionService.submit(buyGoods("洗衣机", 2));
  67. executor.shutdown();
  68. //购买商品的数量
  69. int goodsCount = 2;
  70. for (int i = 0; i < goodsCount; i++) {
  71. //可以获取到最先到的商品
  72. GoodsModel goodsModel = executorCompletionService.take().get();
  73. //将最先到的商品送上楼
  74. moveUp(goodsModel);
  75. }
  76. long et = System.currentTimeMillis();
  77. System.out.println(et + "货物已送到家里咯,哈哈哈!");
  78. System.out.println("总耗时:" + (et - st));
  79. }
  80. }

输出:

  1. 1564653208284开始购物!
  2. 1564653208349购买冰箱下单!
  3. 1564653208349购买洗衣机下单!
  4. 1564653210349洗衣机送到了!
  5. 1564653213350冰箱送到了!
  6. 将商品搬上楼,商品信息:洗衣机,下单时间[1564653208349,1564653210349],耗时:2000
  7. 将商品搬上楼,商品信息:冰箱,下单时间[1564653208349,1564653213350],耗时:5001
  8. 1564653220350货物已送到家里咯,哈哈哈!
  9. 总耗时:12066

从输出中可以看出和我们希望的结果一致,代码中下单顺序是:冰箱、洗衣机,冰箱送货耗时5秒,洗衣机送货耗时2秒,洗衣机先到的,然后被送上楼了,冰箱后到被送上楼,总共耗时12秒,和期望的方案一样。

示例:执行一批任务,然后消费执行结果

代码如下:

  1. package com.itsoku.chat18;
  2. import java.util.ArrayList;
  3. import java.util.Collection;
  4. import java.util.List;
  5. import java.util.concurrent.*;
  6. import java.util.function.Consumer;
  7. /**
  8. * 跟着阿里p7学并发,微信公众号:javacode2018
  9. */
  10. public class Demo15 {
  11. public static void main(String[] args) throws ExecutionException, InterruptedException {
  12. ExecutorService executorService = Executors.newFixedThreadPool(5);
  13. List<Callable<Integer>> list = new ArrayList<>();
  14. int taskCount = 5;
  15. for (int i = taskCount; i > 0; i--) {
  16. int j = i * 2;
  17. list.add(() -> {
  18. TimeUnit.SECONDS.sleep(j);
  19. return j;
  20. });
  21. }
  22. solve(executorService, list, a -> {
  23. System.out.println(System.currentTimeMillis() + ":" + a);
  24. });
  25. executorService.shutdown();
  26. }
  27. public static <T> void solve(Executor e, Collection<Callable<T>> solvers, Consumer<T> use) throws InterruptedException, ExecutionException {
  28. CompletionService<T> ecs = new ExecutorCompletionService<T>(e);
  29. for (Callable<T> s : solvers) {
  30. ecs.submit(s);
  31. }
  32. int n = solvers.size();
  33. for (int i = 0; i < n; ++i) {
  34. T r = ecs.take().get();
  35. if (r != null) {
  36. use.accept(r);
  37. }
  38. }
  39. }
  40. }

输出:

  1. 1564667625648:2
  2. 1564667627652:4
  3. 1564667629649:6
  4. 1564667631652:8
  5. 1564667633651:10

代码中传入了一批任务进行处理,最终将所有处理完成的按任务完成的先后顺序传递给Consumer进行消费了。

示例:异步执行一批任务,有一个完成立即返回,其他取消

这个给大家讲解2种方式。

方式1

使用ExecutorCompletionService实现,ExecutorCompletionService提供了获取一批任务中最先完成的任务结果的能力。

代码如下:

  1. package com.itsoku.chat18;
  2. import java.util.ArrayList;
  3. import java.util.Collection;
  4. import java.util.List;
  5. import java.util.concurrent.*;
  6. import java.util.function.Consumer;
  7. /**
  8. * 跟着阿里p7学并发,微信公众号:javacode2018
  9. */
  10. public class Demo16 {
  11. public static void main(String[] args) throws ExecutionException, InterruptedException {
  12. long startime = System.currentTimeMillis();
  13. ExecutorService executorService = Executors.newFixedThreadPool(5);
  14. List<Callable<Integer>> list = new ArrayList<>();
  15. int taskCount = 5;
  16. for (int i = taskCount; i > 0; i--) {
  17. int j = i * 2;
  18. String taskName = "任务"+i;
  19. list.add(() -> {
  20. TimeUnit.SECONDS.sleep(j);
  21. System.out.println(taskName+"执行完毕!");
  22. return j;
  23. });
  24. }
  25. Integer integer = invokeAny(executorService, list);
  26. System.out.println("耗时:" + (System.currentTimeMillis() - startime) + ",执行结果:" + integer);
  27. executorService.shutdown();
  28. }
  29. public static <T> T invokeAny(Executor e, Collection<Callable<T>> solvers) throws InterruptedException, ExecutionException {
  30. CompletionService<T> ecs = new ExecutorCompletionService<T>(e);
  31. List<Future<T>> futureList = new ArrayList<>();
  32. for (Callable<T> s : solvers) {
  33. futureList.add(ecs.submit(s));
  34. }
  35. int n = solvers.size();
  36. try {
  37. for (int i = 0; i < n; ++i) {
  38. T r = ecs.take().get();
  39. if (r != null) {
  40. return r;
  41. }
  42. }
  43. } finally {
  44. for (Future<T> future : futureList) {
  45. future.cancel(true);
  46. }
  47. }
  48. return null;
  49. }
  50. }

程序输出下面结果然后停止了:

  1. 任务1执行完毕!
  2. 耗时:2072,执行结果:2

代码中执行了5个任务,使用CompletionService执行任务,调用take方法获取最先执行完成的任务,然后返回。在finally中对所有任务发送取消操作(future.cancel(true);),从输出中可以看出只有任务1执行成功,其他任务被成功取消了,符合预期结果。

方式2

其实ExecutorService已经为我们提供了这样的方法,方法声明如下:

  1. <T> T invokeAny(Collection<? extends Callable<T>> tasks)
  2. throws InterruptedException, ExecutionException;

示例代码:

  1. package com.itsoku.chat18;
  2. import java.util.ArrayList;
  3. import java.util.Collection;
  4. import java.util.List;
  5. import java.util.concurrent.*;
  6. /**
  7. * 跟着阿里p7学并发,微信公众号:javacode2018
  8. */
  9. public class Demo17 {
  10. public static void main(String[] args) throws ExecutionException, InterruptedException {
  11. long startime = System.currentTimeMillis();
  12. ExecutorService executorService = Executors.newFixedThreadPool(5);
  13. List<Callable<Integer>> list = new ArrayList<>();
  14. int taskCount = 5;
  15. for (int i = taskCount; i > 0; i--) {
  16. int j = i * 2;
  17. String taskName = "任务" + i;
  18. list.add(() -> {
  19. TimeUnit.SECONDS.sleep(j);
  20. System.out.println(taskName + "执行完毕!");
  21. return j;
  22. });
  23. }
  24. Integer integer = executorService.invokeAny(list);
  25. System.out.println("耗时:" + (System.currentTimeMillis() - startime) + ",执行结果:" + integer);
  26. executorService.shutdown();
  27. }
  28. }

输出下面结果之后停止:

  1. 任务1执行完毕!
  2. 耗时:2061,执行结果:2

输出结果和方式1中结果类似。

来源:http://itsoku.com/course/1/20
posted @ 2022-05-04 20:14  程序员小明1024  阅读(32)  评论(0编辑  收藏  举报