内容提要
文章对比了Spring的@Async与JDK8的CompletableFuture.supplyAsync两种异步方式。实验表明:@Async使用Future.get会阻塞主线程,去掉返回值才真正异步;supplyAsync效果相同,底层有线程池化处理,大量任务不会立即耗尽资源,任务也不会被丢弃。作者强调编程应“不要假定,要证明”,通过实践验证结论。
延伸解读
异步调用中的阻塞陷阱
文章通过实验表明,使用Spring的@Async注解时,如果方法返回Future并在主线程调用get(),主线程会被阻塞,异步效果大打折扣。在Web环境下,这意味着Tomcat等容器的线程无法及时释放,与同步调用差异不大。因此,若不需要返回结果,应避免调用get(),或直接将异步方法设计为void,才能真正释放主线程。
CompletableFuture的线程池化优势
JDK8的CompletableFuture.supplyAsync底层使用了线程池,文章实验显示,即使提交50万个异步任务,资源占用也没有明显增加,任务不会被丢弃,而是排队执行。这避免了频繁创建线程导致的内存溢出问题。但需注意,任务执行时间较长时,总完成时间会很长,且进程停止可能导致未执行任务丢失。
选择异步方式的实践建议
对于不需要返回结果的异步操作,使用CompletableFuture.runAsync比supplyAsync更合适,因为runAsync没有返回值,语义更清晰。而Spring的@Async若需真正异步,应避免返回Future或在调用处不调用get()。文章强调,编程中应遵循“不要假定,要证明”的原则,通过实验验证异步行为,避免想当然。
Q&A
在Web环境下,如果主线程需要等待异步操作的返回结果,异步和同步有什么区别?
区别不大。因为主线程要等待异步返回结果,在结果返回之前,Web服务器(如Tomcat)不会释放线程,所以对于需要等待的操作,异步和同步在资源占用上效果相似。
Spring的@Async注解如何实现真正的异步(不阻塞主线程)?
需要去掉异步方法的返回值,并且调用方不要调用Future.get()。这样主线程不会等待异步线程执行完毕,直接继续执行,从而实现真正的异步。
CompletableFuture.supplyAsync和@Async在异步效果上有什么异同?
两者在实现异步效果上完全一致。supplyAsync底层有线程池化处理,大量任务不会立即耗尽资源,任务也不会被丢弃;而@Async需要正确使用(无返回值)才能达到相同效果。
使用CompletableFuture.supplyAsync时,大量异步任务会导致资源耗尽或任务丢失吗?
不会。实验表明,即使开启50万个异步任务,资源占用并没有明显增多,因为supplyAsync底层有池化处理;任务也不会被丢弃,会继续执行。
如果不关心异步任务的返回值,应该使用CompletableFuture的哪个方法?
应该使用CompletableFuture.runAsync,而不是supplyAsync,因为runAsync更地道,适用于不需要返回结果的场景。
文章通过实验想传达什么编程理念?
文章强调「不要假定,要证明」,即程序员不应想当然,而应通过理论结合实践来验证结论,这样才能写出可维护性好、少bug的代码。