并发异步编程这样写,面试官都得服你
前言
多线程的并发异步编程一直使Java程序员必备的一个技能,面试中常问的多线程代码题,我们写的适合一般是用最简单的Thread,Runnable,Callablle以及FutureTask。但Java程序员写代码的宗旨不就是能使用封装框架的绝不自己创造吗,所以异步并发编程在Java中也有封装好的接口,使得我们的多线程并发编程看上去更加简洁,安全性更高。
基础版
著名数学家华罗庚先生的文章《统筹方法》,这篇文章里介 绍了一个烧水泡茶的例子,文中提到最优的工序应该是下面这样: 其中烧开水的时候可以洗茶壶,洗茶杯,等水烧开了就可以拿茶叶泡茶了。
这里将洗水壶、烧开水、泡茶作为一个线程串行运行,烧茶壶、烧茶杯、拿茶叶作为另外一个线程运行。拿好茶叶之后结果放入泡茶中运行。
、
如果按正常的写法,我们一般都会这样写。
class T2Task implements Callable<String>{
@Override
String call() throws Exception{
// 业务代码
return "龙井";
}
}
class T1Task implements Callable<String>{
FutureTask<String> ft2;
T1Task(FutureTask ft2){
this.ft2 = ft2;
}
@Override
String call() throws Exception{
// 业务代码
// 等待ft2返回结果
String ft = ft2.get();
return "上茶" + ft;
}
}
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
FutureTask<String> ft1 = new FutureTask<>(new T1Task(ft2));
Thread T1 = new Thread(ft1);
T1.start()
Thread T2 = new Thread(ft2);
T2.start();
String res1 = ft1.get();
CompletableFuture
除了上面写的方法,JDK还为我们提供了CompletableFuture这个接口对并发编程进行简化,使代码看上去更简洁,方便。默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创 建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池
// 不带返回值用runAsync,带返回值用supplyAsync
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
// 洗水壶,烧开水
});
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
// 洗茶壶,洗茶杯,拿茶叶
return "龙井";
});
// 任务1和任务2完成后执行合并
CompletableFuture<Void> f3 = CompletableFuture.runAsync(() -> {
f1.thenCombine(f2, (__,tf) -> {
return "上茶" + tf;
});
f3.join();
CompletableFuture相对于基础版的优势也很明显
- 无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要
我们关注;
- 语义更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述“任务 3
要等待任务 1 和任务 2 都完成后才能开始”;
- 代码更简练并且专注于业务逻辑,几乎所有代码都是业务逻辑相关的。
CompletionService
当然,多线程里面当然少不了我们常说的线程池了,上述的例子只有两个线程,但是当线程一多,我们不断的创建,销毁线程需要设计到操作系统里面的申请和释放资源,那这样以来,多线程的优势则会因为资源的操作大打折扣。
所以怎么结合线程池进行异步并发任务的出发,也很重要。在Java中给我们提供了CompletionService来通过线程池批量执行异步任务,并且还提供了异步返回结果获取的方法。
// 创建线程池
ExecutorService executor =
Executors.newFixedThreadPool(3);
// 创建 CompletionService
CompletionService<Integer> cs = new
ExecutorCompletionService<>(executor);
// 异步执行
cs.submit(()->run1());
cs.submit(()->run2());
cs.submit(()->run3());
// 获取异步执行结果
for (int i=0; i<3; i++) {
Integer r = cs.take().get();
executor.execute(()->save(r));
}
结语
多线程并发异步编程对于同步编程来说在各种场景下都有着性能上的绝对优势,学会怎么更快,更优美,更快速的进行多线程并发异步编程是一个非常重要的技能,本文所讲的都是一小部分以及简单的例子,作为一个印子,怎么用好这些方法写出高效,优美的代码,还得靠我们自己去更深入的学习
