文章目录
一、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 种策略。
- CallerRunsPolicy:提交任务的线程自己去执行该任务。
- AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- 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并发编程实战