1、概述
大家好,我是欧阳方超。在上一篇文章中——,我们介绍了CompletableFuture的初步使用,只是在那里我们并没有从代码的角度验证是程序异步执行的,今天就来看一下。
2、为什么我的异步任务没被执行
2.1、真的是异步吗
将上一篇文章中的代码稍作调整:
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "期望后输出");
future.thenAccept(s -> {
System.out.println(s);
});
System.out.println("期望首先输出");
}
}
程序输出结果:
期望后输出
期望首先输出
这个输出结果就值得思考了,为什么不是先输出“期望首先输出”这一行文字,而是先输出了“期望后输出”呢,按理说主线程提交了一个异步任务,异步任务由其他线程执行,主线程不等待异步任务继续往下执行,好吧,其实这种情况就是上面代码中异步任务完成的速度可能比较快,因此thenAccept()方法注册的Consumer函数会在主线程输出第一个语句之前就已经被调用了,从而导致输出了上面的结果。
不想让异步任务执行那么快,那么首先想到的是使用sleep()方法模拟一个耗时任务,具体改动如下。
2.2、模拟耗时任务
在异步任务中模拟耗时任务,代码如下:
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "期望后输出";
});
future.thenAccept(s -> {
System.out.println(s);
});
System.out.println("期望首先输出");
}
}
此时只见程序输出了:
期望首先输出
异步任务中的字符串根本没有输出,为什么呢?实际上,JVM只会等待new出来的线程执行完毕后才会退出,不会等待守护进程,而CompletableFuture底层默认使用了ForkJoinPool.commonPool()方法返回的线程,这些线程都是守护线程,所以主线程一旦结束,不会等待异步任务的完成了,进而JVM也退出了。异步任务没有执行,这显然不是我们想要的结果,有办法解决吗,当然有,那就是使用自定义线程池。
2.4、使用自定义线程池
CompletableFuture默认使用的是ForkJoinPool.commonPool()方法返回的线程,好在CompletableFuture也提供了相关方法允许使用自定义的线程池,代码如下:
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "期望后输出";
}, executorService);
future.thenAccept(s -> {
System.out.println(s);
});
System.out.println("期望首先输出");
}
}
上面代码中我们使用Executors.newFixedThreadPool(1)创建了里面包含一个线程的线程池executorService ,调用supplyAsync()方法就会把异步任务提交给executorService ,该异步任务不会立即执行(只会在合适的时机执行),supplyAsync()方法会返回一个future对象,接着主线程继续往下执行System.out.println(“期望首先输出”)语句,接着异步任务完成,thenAccept()回到才运行,最终程序输出:
期望首先输出
期望后输出
但是这个时候有个问题,就是JVM会一直不退出,因为我们自己创建的线程池依然存活着呢,还在等待任务进来,也就是我们没有对其进行关闭,接下来看如何关闭线程池。
2.4、关闭线程池
其实关闭线程池可以使用shutdown()方法,代码如下:
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "期望后输出";
}, executorService);
future.thenAccept(s -> {
System.out.println(s);
});
executorService.shutdown();
System.out.println("期望首先输出");
}
}
上面的程序输出:
期望首先输出
期望后输出
2.5、关闭线程池——更进一步
如果我们只是使用shutdown()来关闭线程池,它会触发之前提交过来的任务按顺序执行完,并且此时只是不在接收新提交过来的任务;还应该调用boolean awaitTermination(long timeout, TimeUnit unit)方法等待线程池中的任务执行完成,该方法接收两个参数,一个是时长,一个是时长单位,该方法会一直阻塞直到下面条件中的其中一个首先发生:在shutdown()请求之后任务成功完成、超时时间已到、当前线程被打断,如果在设置的超时时间内任务执行完了该方法返回true、如果超时了就返回false,下面的代码中使用了boolean awaitTermination(long timeout, TimeUnit unit)方法:
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "期望后输出";
}, executorService);
future.thenAccept(s -> {
System.out.println(s);
});
executorService.shutdown();
boolean b = executorService.awaitTermination(5, TimeUnit.SECONDS);
if (b) {
System.out.println("没问题");
} else {
System.out.println("有问题");
}
System.out.println("期望首先输出");
}
}
输出结果:
期望后输出
没问题
期望首先输出
awaitTermination()方法等待5s,异步任务中的任务等待1s,在等待时间内异步任务完全有时间执行完,返回此时该方法返回了true,注意,异步任务执行完后future.thenAccept()方法是由主线程执行的,所有输出顺序才是上面的结果。
如果我们将awaitTermination()方法等待1s,异步任务中的任务等待5s,awaitTermination()方法将返回false,程序输出结果也会发生变了,详见下方调整后的程序:
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "期望后输出";
}, executorService);
future.thenAccept(s -> {
System.out.println(s);
});
executorService.shutdown();
boolean b = executorService.awaitTermination(1, TimeUnit.SECONDS);
if (b) {
System.out.println("没问题");
} else {
System.out.println("有问题");
}
System.out.println("期望首先输出");
}
}
输出结果:
有问题
期望首先输出
期望后输出
3、总结
今天的介绍,我们从一个小的异步程序开始,到模拟耗时任务,再到用自定义线程池执行异步任务,最后谈到线程池的关闭,从一些方面了解了为什么有时候异步任务没有被执行。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。