并发编程学习笔记5——线程池、Future、CompletionService、CompletedFuture


一、ThreadPoolExecutor

一般意义上的池化资源,当需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。

线程池是一种生产者 - 消费者模式。线程池的使用方是生产者,线程池本身是消费者。

//简化的线程池,仅用来说明工作原理
class MyThreadPool{
  //利用阻塞队列实现生产者-消费者模式
  BlockingQueue<Runnable> workQueue;
  //保存内部工作线程
  List<WorkerThread> threads 
    = new ArrayList<>();
  // 构造方法
  MyThreadPool(int poolSize, 
    BlockingQueue<Runnable> workQueue){
    this.workQueue = workQueue;
    // 创建工作线程
    for(int idx=0; idx<poolSize; idx++){
      WorkerThread work = new WorkerThread();
      work.start();
      threads.add(work);
    }
  }
  // 提交任务
  void execute(Runnable command){
    workQueue.put(command);
  }
  // 工作线程负责消费任务,并执行任务
  class WorkerThread extends Thread{
    public void run() {
      //循环取任务并执行
      while(true){ ①
        Runnable task = workQueue.take();
        task.run();
      } 
    }
  }  
}

/** 下面是使用示例 **/
// 创建有界阻塞队列
BlockingQueue<Runnable> workQueue = 
  new LinkedBlockingQueue<>(2);
// 创建线程池  
MyThreadPool pool = new MyThreadPool(
  10, workQueue);
// 提交任务  
pool.execute(()->{
    System.out.println("hello");
});

Java 提供的线程池相关的工具类中,最核心的是 ThreadPoolExecutor,它强调的是 Executor,而不是一般意义上的池化资源。

ThreadPoolExecutor 最完备的构造函数有 7 个参数。

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 
  • corePoolSize:表示线程池保有的最小线程数。
  • maximumPoolSize:表示线程池创建的最大线程数。如果等待执行的任务很多,当maximumPoolSize > corePoolSize 时,会增加到最大线程数,加快执行。
  • keepAliveTime & unit:一个线程如果在一段时间内,都没有执行任务,说明很闲。如果一个线程空闲了这个时长,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
  • workQueue:工作队列
  • threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
  • handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
  1. CallerRunsPolicy:提交任务的线程自己去执行该任务。
  2. AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
  3. DiscardPolicy:直接丢弃任务,没有任何异常抛出。
  4. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

Java 并发包里提供了一个线程池的静态工厂类 Executors,利用 Executors 你可以快速创建线程池。但是不建议使用 Executors:Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM。

使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。

使用线程池,还要注意异常处理的问题,例如通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止,但是你却获取不到任何通知,还是捕获所有异常并按需处理。

二、Future

ThreadPoolExecutor 常用的有void execute(Runnable command) 方法,但是却没有办法获取任务的执行结果。

ThreadPoolExecutor 提供 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。这 3 个方法的方法签名如下。

// 提交Runnable任务
Future<?> 
  submit(Runnable task);
// 提交Callable任务
<T> Future<T> 
  submit(Callable<T> task);
// 提交Runnable任务及结果引用  
<T> Future<T> 
  submit(Runnable task, T result);
  • 提交 Runnable 任务 submit(Runnable task) :这个方法的参数是一个 Runnable 接口,Runnable 接口的 run() 方法是没有返回值的,所以返回的 Future 仅可以用来断言任务已经结束了,类似于 Thread.join()。
  • 提交 Callable 任务 submit(Callable<T> task):这个方法的参数是一个 Callable 接口,它只有一个 call() 方法,并且这个方法是有返回值的,所以这个方法返回的 Future 对象可以通过调用其 get() 方法来获取任务的执行结果。
  • 提交 Runnable 任务及结果引用 submit(Runnable task, T result):返回值就是传给 submit() 方法的参数 result。result 相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据。
    用法如下:
ExecutorService executor 
  = Executors.newFixedThreadPool(1);
// 创建Result对象r
Result r = new Result();
r.setAAA(a);
// 提交任务
Future<Result> future = 
  executor.submit(new Task(r), r);  
Result fr = future.get();
// 下面等式成立
fr === r;
fr.getAAA() === a;
fr.getXXX() === x

class Task implements Runnable{
  Result r;
  //通过构造函数传入result
  Task(Result r){
    this.r = r;
  }
  void run() {
    //可以操作result
    a = r.getAAA();
    r.setXXX(x);
  }
}

FutureTask 是一个工具类,有两个构造函数,它们的参数和前面的 submit() 方法类似。

FutureTask 实现了 Runnable 和 Future 接口。由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果。

// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 提交FutureTask 
es.submit(futureTask);
// 获取计算结果
Integer result = futureTask.get();

实现最优的“烧水泡茶”程序
在这里插入图片描述

public class TestFuture {
    static ExecutorService executor=new ThreadPoolExecutor(2,2,0,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Task1 task1 = new Task1();
        Future<String> future1 = executor.submit(task1);
        Task2 task2 = new Task2(future1);
        Future<String> future2 = executor.submit(task2);
        System.out.println(future1.get());
        System.out.println(future2.get());
    }

    static class Task1 implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("洗茶壶");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("洗茶杯");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("拿茶叶");
            TimeUnit.SECONDS.sleep(1);
            return "龙井";
        }
    }

    static class Task2 implements Callable<String>{
        Future<String> task1;

        public Task2(Future<String> task1) {
            this.task1 = task1;
        }

        @Override
        public String call() throws Exception {
            System.out.println("洗水壶");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("烧水");
            TimeUnit.SECONDS.sleep(5);
            String s = task1.get();
            System.out.println("泡茶");
            TimeUnit.SECONDS.sleep(1);
            return "泡好了"+s;
        }
    }
}

三、CompletionService

项目里发现过类似如下代码:

long start=System.currentTimeMillis();

List<Future<Integer>> list=new ArrayList<>();
 Future<Integer> future1 = executorService.submit(() -> {
         TimeUnit.SECONDS.sleep(6);
         return 6;
 });
 list.add(future1);

 Future<Integer> future2 = executorService.submit(() -> {
     TimeUnit.SECONDS.sleep(4);
     return 4;
 });
 list.add(future2);

 Future<Integer> future3 = executorService.submit(() -> {
     TimeUnit.SECONDS.sleep(2);
     return 2;
 });
 list.add(future3);

 for(int i=0;i<list.size();i++){
     Integer result = list.get(i).get();
     System.out.println(result);
 }

 long end=System.currentTimeMillis();
 System.out.println(end-start);

返回结果是6,4,2。即使后面的任务先执行完,也要等之前加入的任务执行完才能拿到结果。假如对结果的处理也是一个费时的操作,那么等待排在前面的任务执行完的时间就浪费掉了。

利用 CompletionService 能解决先完成的任务先处理结果。

CompletionService 的实现原理是内部维护了一个阻塞队列,当任务执行结束就把任务的执行结果的 Future 对象加入到阻塞队列中。

CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个,分别是:

  • ExecutorCompletionService(Executor executor)
  • ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)

这两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。

改进的代码如下:

long start=System.currentTimeMillis();

completionService.submit(() -> {
     TimeUnit.SECONDS.sleep(6);
     return 6;
 });

 completionService.submit(() -> {
     TimeUnit.SECONDS.sleep(4);
     return 4;
 });

 completionService.submit(() -> {
     TimeUnit.SECONDS.sleep(2);
     return 2;
 });


 for(int i=0;i<3;i++){
     Integer result = completionService.take().get();
     System.out.println(result);
 }

此时返回结果是2、4、6。按照先完成的先返回。

利用CompletionService 还可以做到同时向多个同级服务请求,只要收到其中一个响应就可以返回。只要循环中第一次 get 到有效结果就 break。缺点是消耗资源多。


四、CompletedFuture

1. 使用CompletedFuture 实现烧水泡茶

//任务1:洗水壶->烧开水
CompletableFuture<Void> f1 = 
  CompletableFuture.runAsync(()->{
  System.out.println("T1:洗水壶...");
  sleep(1, TimeUnit.SECONDS);

  System.out.println("T1:烧开水...");
  sleep(15, TimeUnit.SECONDS);
});
//任务2:洗茶壶->洗茶杯->拿茶叶
CompletableFuture<String> f2 = 
  CompletableFuture.supplyAsync(()->{
  System.out.println("T2:洗茶壶...");
  sleep(1, TimeUnit.SECONDS);

  System.out.println("T2:洗茶杯...");
  sleep(2, TimeUnit.SECONDS);

  System.out.println("T2:拿茶叶...");
  sleep(1, TimeUnit.SECONDS);
  return "龙井";
});
//任务3:任务1和任务2完成后执行:泡茶
CompletableFuture<String> f3 = 
  f1.thenCombine(f2, (__, tf)->{
    System.out.println("T1:拿到茶叶:" + tf);
    System.out.println("T1:泡茶...");
    return "上茶:" + tf;
  });
//等待任务3执行结果
System.out.println(f3.join());

void sleep(int t, TimeUnit u) {
  try {
    u.sleep(t);
  }catch(InterruptedException e){}
}

好处是:无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要我们关注;代码更简练并且专注于业务逻辑。

2. 创建 CompletableFuture 对象

创建 CompletableFuture 对象主要靠下面代码中展示的这 4 个静态方法。

//使用默认线程池
static CompletableFuture<Void> 
  runAsync(Runnable runnable)
static <U> CompletableFuture<U> 
  supplyAsync(Supplier<U> supplier)
//可以指定线程池  
static CompletableFuture<Void> 
  runAsync(Runnable runnable, Executor executor)
static <U> CompletableFuture<U> 
  supplyAsync(Supplier<U> supplier, Executor executor)  

Runnable 接口的 run() 方法没有返回值,而 Supplier 接口的 get() 方法是有返回值的。

前两个方法和后两个方法的区别在于:后两个方法可以指定线程池参数。

默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议根据不同的业务类型创建不同的线程池。

3. CompletionStage 接口

CompletableFuture 类实现了 CompletionStage 接口,这个接口的方法可以分成几类。

任务是有时序关系的,比如有串行关系、并行关系、汇聚关系
在这里插入图片描述在这里插入图片描述在这里插入图片描述

串行关系

主要有 thenApply、thenAccept、thenRun 和 thenCompose 这四个系列的接口。

CompletionStage<R> thenApply(fn);
CompletionStage<R> thenApplyAsync(fn);
CompletionStage<Void> thenAccept(consumer);
CompletionStage<Void> thenAcceptAsync(consumer);
CompletionStage<Void> thenRun(action);
CompletionStage<Void> thenRunAsync(action);
CompletionStage<R> thenCompose(fn);
CompletionStage<R> thenComposeAsync(fn);
  • thenApply 系列函数里参数 fn 的类型是接口 Function<T, R>,这个接口里与 CompletionStage 相关的方法是 R apply(T t),这个方法既能接收参数也支持返回值,所以 thenApply 系列方法返回的是
    CompletionStage<R>。
  • thenAccept 系列方法里参数 consumer 的类型是接口Consumer<T>,这个接口里与 CompletionStage 相关的方法是 void accept(T t),这个方法支持参数,但却不支持回值,所以 thenAccept 系列方法返回的是CompletionStage<Void>。
  • thenRun 系列方法里 action 的参数是 Runnable,所以 action 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是CompletionStage<Void>。
  • thenCompose 系列方法,会新创建出一个子流程,最终结果和 thenApply 系列是相同的。

AND 汇聚关系

三种参数区别同上。

CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);

OR 汇聚关系

三种参数区别同上。

CompletionStage applyToEither(other, fn);
CompletionStage applyToEitherAsync(other, fn);
CompletionStage acceptEither(other, consumer);
CompletionStage acceptEitherAsync(other, consumer);
CompletionStage runAfterEither(other, action);
CompletionStage runAfterEitherAsync(other, action);

使用示例:

CompletableFuture<String> f1 = 
  CompletableFuture.supplyAsync(()->{
    int t = getRandom(5, 10);
    sleep(t, TimeUnit.SECONDS);
    return String.valueOf(t);
});

CompletableFuture<String> f2 = 
  CompletableFuture.supplyAsync(()->{
    int t = getRandom(5, 10);
    sleep(t, TimeUnit.SECONDS);
    return String.valueOf(t);
});

CompletableFuture<String> f3 = 
  f1.applyToEither(f2,s -> s);

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

异常处理

fn、consumer、action 它们的核心方法都不允许抛出可检查异常,但是却无法限制它们抛出运行时异常。CompletionStage 接口有如下方法:

CompletionStage exceptionally(fn);
CompletionStage<R> whenComplete(consumer);
CompletionStage<R> whenCompleteAsync(consumer);
CompletionStage<R> handle(fn);
CompletionStage<R> handleAsync(fn);
  • exceptionally() 的使用非常类似于 try{}catch{}中的 catch{}
  • whenComplete() 和 handle() 系列方法就类似于 try{}finally{}中的 finally{},区别在于 whenComplete() 不支持返回结果,而 handle() 是支持返回结果的。
CompletableFuture<Integer> 
  f0 = CompletableFuture
    .supplyAsync(()->7/0))
    .thenApply(r->r*10)
    .exceptionally(e->0);
System.out.println(f0.join());

总结

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


思考题

1、使用 CompletionService 实现了一个询价应用的核心功能,需要计算出最低报价并返回,下面的示例代码尝试实现这个需求,你看看是否存在问题呢?

// 创建线程池
ExecutorService executor = 
  Executors.newFixedThreadPool(3);
// 创建CompletionService
CompletionService<Integer> cs = new 
  ExecutorCompletionService<>(executor);
// 异步向电商S1询价
cs.submit(()->getPriceByS1());
// 异步向电商S2询价
cs.submit(()->getPriceByS2());
// 异步向电商S3询价
cs.submit(()->getPriceByS3());
// 将询价结果异步保存到数据库
// 并计算最低报价
AtomicReference<Integer> m =
  new AtomicReference<>(Integer.MAX_VALUE);
for (int i=0; i<3; i++) {
  executor.execute(()->{
    Integer r = null;
    try {
      r = cs.take().get();
    } catch (Exception e) {}
    save(r);
    m.set(Integer.min(m.get(), r));
  });
}
return m;

:保存结果和计算最小值的地方没有等处理完就走到 return 了,另外 m 的get 和set 方法不是原子操作。个人认为计算最小值不需要并发进行,可以在主线程操作。


2、创建采购订单的时候,需要校验一些规则,例如最大金额是和采购员级别相关的。有同学利用 CompletableFuture 实现了这个校验的功能,逻辑很简单,首先是从数据库中把相关规则查出来,然后执行规则校验。你觉得他的实现是否有问题呢?

//采购订单
PurchersOrder po;
CompletableFuture<Boolean> cf = 
  CompletableFuture.supplyAsync(()->{
    //在数据库中查询规则
    return findRuleByJdbc();
  }).thenApply(r -> {
    //规则校验
    return check(po, r);
});
Boolean isOk = cf.join();

:没有做异常处理。异步操作中包含数据库操作,应该单独分配一个线程池。


参考资料:王宝令----Java并发编程实战

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值