ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

面试官:实现异步的 8 种方式,你知道几个?

面试官:实现异步的 8 种方式,你知道几个? 一、为什么面试官总爱问异步异步编程几乎是每个后端开发绕不开的话题。无论是多线程并发、接口超时优化、批量任务处理还是高并发场景下的性能提升异步都在其中扮演核心角色。面试官问「实现异步的 8 种方式」表面上考的是语法和 API 掌握程度实际考察的是你对以下三件事的理解任务如何被提交是一次性线程还是复用线程池结果如何被获取阻塞等待、轮询、回调还是响应式订阅异步如何被管理异常怎么处理线程怎么回收上下游怎么解耦本文将以 Java 为主线系统梳理实现异步的 8 种典型方式并给出可运行的代码示例、适用场景和面试答题思路。文中涉及的方案如下Thread 线程与 RunnableFuture 与 FutureTask线程池 ThreadPoolExecutorCompletableFuture 组合式异步Spring 的 Async 注解回调函数与监听器消息队列 MQ 异步解耦响应式编程 Reactor / RxJava需要提前说明这 8 种方式并非完全互斥很多时候是层层递进、相互配合的关系。理解它们之间的演化逻辑比单纯背 API 更有价值。二、理解同步与异步的本质差异在写代码之前先明确两个高频混淆的概念同步/异步描述的是「调用方式」阻塞/非阻塞描述的是「线程状态」。2.1 同步调用同步调用中调用方发起请求后必须等待结果返回期间无法继续执行后续逻辑。对于慢 IO 场景线程的大量时间被耗费在等待上。public String syncCall() { // 线程在这里阻塞直到远程接口返回 String result remoteService.query(); return result; }2.2 异步调用异步调用中调用方提交任务后立即返回不等待结果。当任务执行完成时通过回调、Future 或消息等方式通知调用方调用方继续处理其他事情。public FutureString asyncCall() { // 立即返回一个凭证任务在后台执行 return executor.submit(() - remoteService.query()); }异步的本质是把「等待」从当前线程中剥离出去让线程资源去做更有价值的事情从而提升吞吐量。下面正式进入 8 种实现方式。三、方式一Thread 线程与 Runnable这是最原始、最基础的异步实现方式。只要新起一个线程执行任务当前调用线程就可以继续向下执行天然具备「异步」的特征。3.1 继承 Thread 类public class AsyncByThread { public static void main(String[] args) { Thread thread new Thread(() - { System.out.println(异步任务执行中线程 Thread.currentThread().getName()); }); thread.start(); System.out.println(主线程继续执行); } }3.2 实现 Runnable 接口相比继承 Thread实现 Runnable 更符合「组合优于继承」的原则任务逻辑与线程本身解耦方便提交给线程池复用。public class AsyncByRunnable { public static void main(String[] args) { Runnable task () - { System.out.println(Runnable 任务执行线程 Thread.currentThread().getName()); }; new Thread(task).start(); System.out.println(提交后立即返回); } }3.3 这种方式的问题每提交一个任务就创建一个线程线程的创建和销毁开销大。无法方便地获取任务执行结果出现异常时也难以统一处理。线程数量不可控高并发下容易耗尽系统资源。因此裸 Thread 只适合极轻量、频率低的场景。在实际工程中它更多作为理解后续线程池和 Future 的起点。四、方式二Future 与 FutureTaskThread 和 Runnable 最大的短板是「拿不到返回值」。JDK 1.5 引入的Callable和Future解决了这个问题。4.1 Callable 返回结果Callable与Runnable类似但它的call()方法可以返回结果并能抛出受检异常。CallableString task () - { Thread.sleep(2000); return 任务完成; };4.2 FutureTask 结合 ThreadFutureTaskString futureTask new FutureTask(task); new Thread(futureTask).start(); // 阻塞等待结果 String result futureTask.get(); System.out.println(result);4.3 Future 常用方法方法作用get()阻塞获取结果get(timeout, unit)超时获取结果避免无限等待isDone()判断任务是否完成cancel(boolean)尝试取消任务4.4 Future 的局限Future 开创了「异步任务 结果凭证」的模式但仍有明显不足get()是阻塞的调用方最终还是会在某个点等待。多个任务之间难以编排例如「A 完成后执行 B」「多个任务全部完成后汇总」。缺少异常回调机制错误处理不优雅。这些痛点恰好由后面的 CompletableFuture 补齐。五、方式三线程池 ThreadPoolExecutor线程池是异步编程中最重要的基础设施。它通过复用固定数量的线程来执行大量任务既保留了异步特性又解决了裸 Thread 反复创建销毁的开销问题。5.1 使用 Executors 快速创建ExecutorService executor Executors.newFixedThreadPool(5); executor.submit(() - { System.out.println(复用线程池中的线程执行任务); }); executor.shutdown();5.2 手工构造 ThreadPoolExecutor《阿里巴巴 Java 开发手册》明确建议不要直接使用 Executors 创建线程池而应通过 ThreadPoolExecutor 显式指定参数以便清晰控制线程池行为。ThreadPoolExecutor executor new ThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 60, // 空闲线程存活时间 TimeUnit.SECONDS, new ArrayBlockingQueue(100), // 有界队列 new ThreadFactoryBuilder().setNameFormat(async-pool-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );5.3 七个核心参数参数含义corePoolSize核心线程数线程池中长期保留的线程数量maximumPoolSize最大线程数队列满后可扩容到的上限keepAliveTime非核心线程空闲多久后被回收unitkeepAliveTime 的时间单位workQueue任务队列缓存尚未被执行的提交任务threadFactory线程工厂统一命名和设置线程属性handler拒绝策略线程池和队列都满时的处理方式5.4 线程池配合 Future 异步获取结果ExecutorService pool Executors.newFixedThreadPool(3); FutureInteger future pool.submit(() - { Thread.sleep(1000); return 42; }); System.out.println(主线程可以继续做其他事); Integer result future.get(); System.out.println(异步结果 result); pool.shutdown();线程池解决了「线程复用」和「资源管控」两大问题是大多数异步方案的地基。它通常不会单独出现而是与 Future、CompletableFuture、Spring Async 等方式配合使用。六、方式四CompletableFuture 组合式异步CompletableFuture 是 JDK 8 引入的异步编程利器也是面试中的绝对高频点。它在 Future 的基础上实现了「可完成」「可组合」「可回调」的异步编排能力是同步代码思维向异步代码思维过渡的关键工具。6.1 最简单的异步执行CompletableFutureString future CompletableFuture.supplyAsync(() - { return Hello; }); System.out.println(future.get());6.2 任务串联thenApply / thenComposeCompletableFuture.supplyAsync(() - 订单查询) .thenApply(order - order - 库存检查) .thenApply(order - order - 支付处理) .thenAccept(System.out::println);6.3 任务合并thenCombine / allOf / anyOfCompletableFutureString userFuture CompletableFuture.supplyAsync(() - 用户信息); CompletableFutureString orderFuture CompletableFuture.supplyAsync(() - 订单信息); CompletableFutureString result userFuture.thenCombine(orderFuture, (user, order) - 聚合结果 user order); System.out.println(result.get());当多个任务没有依赖关系时可以并行执行后再汇总结果显著缩短接口总耗时。6.4 异常处理exceptionally / handleCompletableFutureString future CompletableFuture.supplyAsync(() - { if (true) { throw new RuntimeException(查询失败); } return 成功; }).exceptionally(ex - { System.err.println(捕获异常 ex.getMessage()); return 降级数据; }); System.out.println(future.get());6.5 自定义线程池CompletableFuture 默认使用 ForkJoinPool.commonPool。生产环境建议显式传入业务线程池避免公共池被占满影响其他异步任务。ExecutorService bizPool Executors.newFixedThreadPool(10); CompletableFutureString future CompletableFuture.supplyAsync( () - 业务异步任务, bizPool);CompletableFuture 适合处理「多个异步任务的编排、聚合、异常兜底」是现代 Java 异步开发的首选方案务必重点掌握。七、方式五Spring 的 Async 注解Spring 框架对异步提供了声明式支持。方法上添加Async注解后Spring 会通过代理将方法调用分派到线程池中异步执行业务代码几乎无侵入。7.1 开启异步支持Configuration EnableAsync public class AsyncConfig { Bean(taskExecutor) public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(200); executor.setThreadNamePrefix(spring-async-); executor.initialize(); return executor; } }7.2 在方法上标注 AsyncService public class OrderService { Async(taskExecutor) public CompletableFutureString sendNotify(String orderId) { // 这里是异步执行的逻辑 System.out.println(异步发送通知 orderId); return CompletableFuture.completedFuture(通知已发送); } }7.3 使用注意事项Async依赖 Spring 代理同类内部方法调用不会触发异步需要通过容器 Bean 调用。Async方法不能声明为 private 或 final否则代理无法增强。无返回值时用 void有返回值时建议返回 Future、CompletableFuture 或 ListenableFuture。必须配置自定义线程池并显式指定避免使用默认 SimpleAsyncTaskExecutor因为它每次都可能新建线程。Async 的优点是把线程池细节与业务代码解耦适合快速实现「操作完成后异步发送短信、邮件、埋点」等旁路逻辑。八、方式六回调函数与监听器回调Callback是一种更经典的异步思想调用方把一个函数或对象传给被调用方任务完成后由被调用方反向调用该函数从而避免调用方轮询或阻塞等待。8.1 Java 函数式接口实现回调// 定义回调接口 FunctionalInterface interface Callback { void onComplete(String data); } public void asyncLoad(Callback callback) { new Thread(() - { // 模拟耗时加载 String data loadData(); callback.onComplete(data); }).start(); } asyncLoad(data - System.out.println(数据加载完成 data));8.2 监听器模式监听器是回调思想在事件驱动场景下的延伸。例如在 Netty、Spring 事件、异步消息消费等框架中监听器无处不在。public interface EventListener { void onEvent(Event event); } public class EventBus { private ListEventListener listeners new ArrayList(); public void register(EventListener listener) { listeners.add(listener); } public void publish(Event event) { listeners.forEach(listener - listener.onEvent(event)); } }回调方式将「结果处理逻辑」前置传入调用方无需显式获取结果。它的缺点是复杂场景下容易出现「回调地狱」错误处理和流程编排不如 CompletableFuture 和响应式编程优雅。九、方式七消息队列 MQ 异步解耦消息队列是分布式系统中实现异步的典型手段。与前述基于线程的方式不同MQ 将异步拓展到「进程之间、服务之间」通过消息中间件解耦生产者和消费者。9.1 消息队列如何实现异步生产者处理完主流程后将消息发送到 MQ立即返回成功。消费者异步地从 MQ 拉取消息并处理。如果消费失败可以通过重试、死信队列等机制保证可靠性。9.2 典型场景订单创建后异步扣库存// 生产者只负责发消息不等待库存扣减完成 Transactional public String createOrder(Order order) { orderMapper.insert(order); // 发送异步消息 mqProducer.send(order-created-topic, order.getId()); return 订单创建成功; } // 消费者异步扣减库存 KafkaListener(topics order-created-topic) public void deductStock(String orderId) { stockService.deduct(orderId); System.out.println(库存扣减完成 orderId); }在这个例子中创建订单接口只需把消息投递到 Kafka 后立即返回库存扣减由消费者异步完成。这样订单服务无需感知库存服务的执行耗时接口响应时间大幅下降。9.3 MQ 实现异步的注意点发送方和消费方要有唯一消息 ID配合日志实现可追踪、可对账。消费失败要有重试机制同时避免重复消费可通过幂等键保证最终一致性。重要链路需要监控消息积压、消费延迟和死信队列。十、方式八响应式编程 Reactor / RxJava响应式编程是基于数据流和变化传播的异步编程范式。它把数据源看作持续发射事件的流调用方通过声明式操作符对流进行过滤、转换、合并并在数据到达时自动触发处理逻辑。10.1 响应式编程的核心思想与 CompletableFuture 强调任务编排不同响应式编程更强调数据流处理和背压控制适合处理海量、连续、非阻塞的异步数据例如网关转发、实时行情、日志采集等场景。10.2 Project Reactor 的 Mono 与 FluxProject Reactor 是 Spring WebFlux 的响应式基础核心类型有两种Mono表示 0 到 1 个元素的异步序列例如一次远程调用返回单个结果。Flux表示 0 到 N 个元素的异步序列例如实时消息流、批量数据流。10.3 异步非阻塞示例MonoString userMono reactorClient.getUser(1L) .map(user - 用户 user.getName()) .doOnNext(System.out::println); FluxOrder orderFlux orderClient.listOrders() .filter(order - order.getAmount() 100) .flatMap(order - payService.pay(order)) .doOnError(ex - System.err.println(处理异常 ex.getMessage())); userMono.subscribe(); orderFlux.subscribe();上面的代码不会阻塞主线程。数据到达时才触发 map、filter、flatMap 等操作符整个过程可以在少量线程上并发处理大量请求。10.4 RxJava 与背压RxJava 是响应式编程在 JVM 上的经典实现常见类型有 Observable、Flowable、Single、Maybe、Completable。其中 Flowable 支持背压可以让下游根据自己的处理能力控制上游数据流速。Flowable.range(1, 1000) .onBackpressureBuffer() .observeOn(Schedulers.io()) .subscribe(System.out::println);背压机制解决的核心问题是当上游生产速度远快于下游消费速度时如何避免数据积压导致内存溢出。10.5 适用场景与局限适合高并发网关、实时流处理、数据管道、长生命周期连接等场景。局限学习曲线陡峭调试链路复杂团队没有响应式经验时容易过度设计。建议普通 CRUD 业务不必强行响应式当非阻塞 IO 和背压是真实需求时再引入 Reactor 或 RxJava。十一、8 种方式对比与选型建议实现方式核心特点典型场景Thread / Runnable最简单线程不可控极轻量、低频任务Future / FutureTask可获取结果但 get 阻塞简单异步任务线程池线程复用资源可控通用异步任务CompletableFuture可组合、可回调多任务编排、聚合Spring Async声明式无侵入发送通知、埋点回调与监听器事件驱动框架扩展、事件处理消息队列 MQ服务间解耦、削峰订单异步、系统解耦响应式编程非阻塞、背压高并发网关、流处理选择建议单机内简单异步优先线程池和 CompletableFuture微服务间需要解耦、削峰时使用 MQ如果业务就是高并发、非阻塞 IO 流式场景再考虑响应式编程。十二、总结实现异步的 8 种方式并不是孤立的知识点而是围绕任务提交、结果获取、资源管理、业务解耦不断演进的完整体系。面试回答时可以先从 Thread 和 Future 引出痛点再到线程池然后重点讲 CompletableFuture 的组合编排能力结合业务场景补充 Spring Async、MQ 和响应式编程会显得既有广度又有工程落地经验。把这 8 种方式的演进逻辑讲清楚比记住零散 API 更能打动面试官。
返回列表