面试题答案
一键面试- 使用CompletableFuture实现异步任务
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class AsyncTaskWithCompletableFuture { private static final ExecutorService executorService; static { AtomicInteger threadNumber = new AtomicInteger(1); executorService = Executors.newFixedThreadPool(10, r -> { Thread t = new Thread(r); t.setName("CustomThreadPool-" + threadNumber.getAndIncrement()); return t; }); } public static CompletableFuture<String> asyncTask() { return CompletableFuture.supplyAsync(() -> { // 模拟从数据库读取数据 System.out.println("从数据库读取数据..."); try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } String data = "读取到的数据"; // 简单处理数据 String processedData = data + " 已处理"; return processedData; }, executorService); } public static void main(String[] args) { asyncTask().thenAccept(System.out::println).join(); executorService.shutdown(); } }
- 通过合理的线程池配置以及CompletableFuture的方法调用来提升执行效率
- 线程池配置:
- 线程池大小:根据任务类型和硬件资源来确定线程池大小。对于I/O密集型任务(如数据库读取),线程池大小可以设置得相对较大,因为线程在等待I/O操作完成时可以释放CPU资源。例如,如果任务主要是从数据库读取数据,并且服务器有多个CPU核心,可以设置线程池大小为CPU核心数的2 - 3倍。在上述代码中,我们创建了一个固定大小为10的线程池,这是一个可以根据实际情况调整的参数。
- 线程命名:为线程池中的线程命名有助于在日志和监控中更好地识别线程,如上述代码中通过
AtomicInteger
为线程命名。
- CompletableFuture方法调用:
- supplyAsync:用于异步执行有返回值的任务。它接受一个
Supplier
和一个Executor
作为参数,这样可以将任务提交到指定的线程池执行,避免使用默认的ForkJoinPool.commonPool()
,因为默认线程池可能在高负载下出现性能问题。 - thenApply、thenAccept、thenRun等后续处理方法:这些方法可以在异步任务完成后,以同步或异步的方式进行后续操作。例如
thenApply
用于对异步任务的返回值进行转换,thenAccept
用于消费返回值,thenRun
用于执行无参数的后续操作。合理使用这些方法可以减少不必要的线程切换和等待时间。例如,如果后续操作不需要返回值,使用thenAccept
或thenRun
可以避免创建额外的CompletableFuture
对象。 - thenCompose:当一个
CompletableFuture
的结果需要作为另一个CompletableFuture
的输入时,使用thenCompose
可以避免嵌套的CompletableFuture
,使代码更简洁,同时也能提高性能,因为它可以更好地管理异步操作的执行顺序。 - allOf和anyOf:
allOf
用于等待所有给定的CompletableFuture
完成,anyOf
用于等待任意一个CompletableFuture
完成。如果有多个独立的异步任务需要执行,可以使用allOf
将它们合并,然后在所有任务完成后进行统一处理,这样可以充分利用多线程并行执行的优势,提高整体效率。
- supplyAsync:用于异步执行有返回值的任务。它接受一个
- 线程池配置: