目录
- 1.同步调用与异步调用
- 1.1.同步调用
- 1.2.异步调用
- 1.3.总结
- 2.注解 @Async 介绍
- 2.1.用在方法上
- 2.2.用在类上
- 3.使用演示
- 3.1.在启动类或者配置类上增加 @EnableAsync 注解
- 3.2.在异步方法上增加 @Async 注解
- 3.3.调用异步方法
- 3.4.测试
- 3.5.其它说明
- 4.注意事项
- 4.1.@Async 注解失效的常见情况
- 4.2.使用 @Async 注解可能会导致循环依赖
- 4.2.1.示例
- 4.2.2.原因分析
- 4.2.3.解决方案
1.同步调用与异步调用
在 Java 中,同步调用和异步调用是两种不同的操作方式,用于处理方法调用和任务执行。
1.1.同步调用
(1)定义:同步调用指的是调用方法时,调用者会等待被调用的方法执行完成后才继续执行后续的代码。也就是说,方法调用是阻塞的,当前线程会被阻塞直到方法执行结束并返回结果。
(2)示例:
public class SyncExample {
public static void main(String[] args) {
System.out.println("Start");
//同步调用
String result = longRunningTask();
System.out.println("Result: " + result);
System.out.println("End");
}
public static String longRunningTask() {
try {
//模拟长时间运行的任务
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Task Completed";
}
}
(3)解释:
在上述代码中,longRunningTask()
方法会暂停当前线程 3 秒钟,然后返回结果。在 main
方法中,longRunningTask()
的调用是同步的,System.out.println("Result: " + result);
会等待 longRunningTask()
完成后才执行。
1.2.异步调用
(1)定义:异步调用指的是调用方法时,调用者不会等待方法执行完成,而是立即继续执行后续的代码。被调用的方法在后台线程中执行,调用者可以通过回调、Future 模式或其他机制获取结果。
(2)示例:
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static void main(String[] args) {
System.out.println("Start");
//异步调用
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> longRunningTask());
//继续执行其他操作
System.out.println("Doing other work while waiting for result...");
//获取异步任务的结果(会阻塞,直到任务完成)
future.thenAccept(result -> System.out.println("Result: " + result));
System.out.println("End");
}
public static String longRunningTask() {
try {
//模拟长时间运行的任务
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Task Completed";
}
}
(3)解释:在上述代码中,longRunningTask()
被异步执行,通过 CompletableFuture.supplyAsync
方法。在异步执行期间,System.out.println("Doing other work while waiting for result...");
会继续执行,不会被阻塞。future.thenAccept(result -> System.out.println("Result: " + result));
会在任务完成后处理结果。
1.3.总结
- 同步调用:
- 特点:调用方在发起方法调用后,必须等待被调用的方法完成并返回结果之后,才能继续执行后续的操作(即会阻塞)。在同步调用中,调用方和被调用方的执行顺序是严格按照调用顺序进行的。
- 适用场景:
- 依赖顺序:当某个操作必须在另一个操作完成后才能进行时。
- 简单任务:当任务执行时间短且没有并发问题时。
- 确保执行顺序:需要确保多个步骤按顺序完成,比如数据库事务处理。
- 异步调用:
- 特点:调用方发起方法调用后,不必等待被调用的方法完成,而是可以继续执行后续的操作。被调用的方法将在后台线程中运行,完成后会通知调用方结果或执行回调。
- 适用场景:
- 任务需要较长时间完成,阻塞主线程会影响程序的响应性。
- 需要处理多个任务时,通过异步调用可以提高并发处理能力。
- 用户界面应用中,异步操作可以防止界面冻结,提升用户体验。
2.注解 @Async 介绍
@Async
注解中的 Async 是单词 Asynchronous
的缩写。Asynchronous 指的是“异步的”,即操作或方法的执行不会阻塞当前线程,允许在后台执行任务的同时继续进行其他操作。使用 @Async 注解可以让 Spring 在后台线程池中异步执行被注解的方法,从而提升应用的性能和响应能力。@Async
注解的代码如下所示:
package org.springframework.scheduling.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.aot.hint.annotation.Reflective;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective
public @interface Async {
String value() default "";
}
@Async
注解用在方法和类上的作用分别如下:
2.1.用在方法上
(1)功能:标记方法为异步执行,这意味着方法将在一个新的线程中执行,不会阻塞调用该方法的线程。并且方法所在的类必须要被 Spring 作为 Bean 管理。
(2)限制:只对 public
方法有效,且方法返回类型为 void
或者 Future
。
(3)例子:
@Async
public CompletableFuture<String> asyncMethod() {
// 执行异步任务
}
2.2.用在类上
(1)功能:标记类中的所有 public
方法都为异步执行。这个类必须要被 Spring 作为 Bean 管理,并且所有被 @Async
注解的方法都会异步执行。
(2)限制:类上使用 @Async 不一定能保证类中所有方法都异步执行,特别是如果方法被类内部直接调用时,例如方法 asyncMethod2
在同一类内直接调用方法 asyncMethod1
,asyncMethod1
会在当前线程中同步执行,而不是异步执行。
(3)例子:
@Async
@Service
public class MyService {
public CompletableFuture<String> asyncMethod1() {
// 执行异步任务
}
public CompletableFuture<String> asyncMethod2() {
// 执行异步任务
}
}
3.使用演示
一般来说,@Async
注解用在方法上来指定特定方法异步执行,标记整个类的方式较少使用。
3.1.在启动类或者配置类上增加 @EnableAsync 注解
例如,在 Spring Boot 项目的启动类上增加 @EnableAsync 注解:
@EnableAsync //开启异步调用
@SpringBootApplication
public class MyExampleApplication {
public static void main(String[] args) {
SpringApplication.run(MyExampleApplication.class, args);
}
}
@EnableAsync 注解用于启用 Spring 框架的异步方法执行功能。它使得应用程序能够处理被 @Async 注解标记的方法,确保这些方法在独立的线程中异步执行。将 @EnableAsync 注解添加到配置类或者启动类上后,Spring 将自动配置所需的支持,并允许 @Async 注解在应用程序中生效。
3.2.在异步方法上增加 @Async 注解
package com.example.myexample.service.async;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MyAsyncTask {
@Async
public void notifyUser() {
log.info("asyncTask 方法所处的线程:{}", Thread.currentThread().getName());
//模拟处理异步任务
try {
Thread.sleep(3000);
log.info("通知用户保存订单成功");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
3.3.调用异步方法
package com.example.myexample.controller;
import com.example.myexample.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("asyncTest")
public class AsyncTestController {
@Resource
private OrderService orderService;
@PostMapping
public String saveOrder() {
orderService.saveOrder();
return "success";
}
}
package com.example.myexample.service;
public interface OrderService {
void saveOrder();
}
package com.example.myexample.service.impl;
import com.example.myexample.service.OrderService;
import com.example.myexample.service.async.MyAsyncTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private MyAsyncTask myAsyncTask;
@Override
public void saveOrder() {
log.info("saveOrder 方法所处的线程:{}", Thread.currentThread().getName());
log.info("保存订单");
//调用异步方法
myAsyncTask.notifyUser();
postProcess();
}
public void postProcess() {
log.info("postProcess 方法所处的线程:{}", Thread.currentThread().getName());
}
}
上述代码只包括了调用异步方法的核心部分,其它部分代码(例如 Controller 层)并未给出。
3.4.测试
使用 Postman 进行测试:
http://localhost:8080/asyncTest/
日志打印结果如下,从中可以发现:方法 saveOrder
和 notifyUser
在不同的线程中执行了,即 notifyUser
确实被异步调用了。
3.5.其它说明
如果要将异步方法改为有返回值,可以将返回类型从 void
改为 Future<T>
或 CompletableFuture<T>
。这里我们使用 CompletableFuture 作为返回类型:
@Async
public CompletableFuture<String> notifyUser2() {
log.info("asyncTask 方法所处的线程:{}", Thread.currentThread().getName());
// 模拟处理异步任务
try {
Thread.sleep(3000);
log.info("通知用户保存订单成功");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return CompletableFuture.completedFuture("通知用户保存订单成功");
}
public void saveOrder2() {
log.info("saveOrder 方法所处的线程:{}", Thread.currentThread().getName());
log.info("保存订单");
CompletableFuture<String> future = myAsyncTask.notifyUser2();
//在需要时可以调用 get() 来阻塞当前线程直到异步任务完成
try {
String result = future.get(); // 这将阻塞直到异步任务完成
System.out.println("任务结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
有关 Future 模式的相关知识可以参考 Java 并发编程面试题——Future 模式这篇文章。
4.注意事项
4.1.@Async 注解失效的常见情况
@Async
注解在某些情况下可能会失效,包括:
- 方法定义不符合要求:
@Async
方法必须是public
,并且不能是final
、static
或private
。 - 类未被 Spring 扫描:确保含有
@Async
注解的类被 Spring 扫描到。通常需要将类放在@Component
、@Service
或其他 Spring 管理的组件中。 - 缺少 @EnableAsync 注解:在配置类上未添加
@EnableAsync
注解,Spring 不会启用异步处理。 - 方法调用在同一类内部:
@Async
方法被同一类的其他方法调用时不会生效,因为 Spring 的代理机制无法处理同一类内的调用。 - 返回值处理不当:异步方法通常返回
Future
或void
,确保返回值类型正确处理。
4.2.使用 @Async 注解可能会导致循环依赖
4.2.1.示例
需要注意的是,如果代码编写不当,在使用 @Async
注解可能会导致循环依赖,例如:
@Service
public class CDTestsService1Impl implements CDTestsService1 {
@Autowired
private CDTestsService2 cdTestsService2;
@Async
@Override
public void performAsyncTask() {
System.out.println("performAsyncTask...");
}
}
@Service
public class CDTestsService2Impl implements CDTestsService2 {
@Autowired
private CDTestsService1 cdTestsService1;
@Override
public void performTask() {
cdTestsService1.performAsyncTask();
}
}
@RestController
@RequestMapping("asyncTest")
public class AsyncTestController {
@Resource
private CDTestsService2 cdTestsService2;
@PostMapping("/cdTest")
public String cdTest() {
cdTestsService2.performTask();
return "success";
}
}
当启动项目时,会出现循环依赖的异常提示:
4.2.2.原因分析
- 循环依赖:
- CDTestsService1Impl 依赖于 CDTestsService2Impl,并且 CDTestsService2Impl 依赖于 CDTestsService1Impl。这种相互依赖会导致 Spring 容器在创建这些 bean 时发生问题。
- 当 Spring 容器创建 CDTestsService1Impl 时,它需要注入 CDTestsService2Impl。接着,创建 CDTestsService2Impl 时,它需要注入 CDTestsService1Impl。这样,就会形成一个死循环,因为两个 bean 互相等待对方被完全创建和注入。
- @Async 注解的影响:
- @Async 注解用于异步执行方法。这意味着 performAsyncTask 方法会在不同的线程中运行,因此不会阻塞当前线程。但是,Spring 在执行异步方法时,仍然需要确保异步方法的 bean 实例在调用之前已完全创建和注入。
- 由于 performAsyncTask 是异步的,Spring 在调用这个方法时可能会遇到由于循环依赖未完全解决而引发的异常。
4.2.3.解决方案
(1)使用构造函数注入
使用构造函数注入可以避免循环依赖问题。构造函数注入在 bean 创建时确保所有依赖项都已解决,因此更可靠。
@Service
public class CDTestsService1Impl implements CDTestsService1 {
private final CDTestsService2 cdTestsService2;
@Autowired
public CDTestsService1Impl(CDTestsService2 cdTestsService2) {
this.cdTestsService2 = cdTestsService2;
}
@Async
@Override
public void performAsyncTask() {
System.out.println("performAsyncTask...");
}
}
@Service
public class CDTestsService2Impl implements CDTestsService2 {
private final CDTestsService1 cdTestsService1;
@Autowired
public CDTestsService2Impl(CDTestsService1 cdTestsService1) {
this.cdTestsService1 = cdTestsService1;
}
@Override
public void performTask() {
cdTestsService1.performAsyncTask();
}
}
(2)使用 @Lazy 注解
@Lazy
注解可以推迟 bean 的初始化,解决循环依赖问题。你可以将 @Lazy
注解添加到字段注入中,以便在需要时才加载依赖。
@Service
public class CDTestsService1Impl implements CDTestsService1 {
@Autowired
@Lazy
private CDTestsService2 cdTestsService2;
@Async
@Override
public void performAsyncTask() {
System.out.println("performAsyncTask...");
}
}
@Service
public class CDTestsService2Impl implements CDTestsService2 {
@Autowired
@Lazy
private CDTestsService1 cdTestsService1;
@Override
public void performTask() {
cdTestsService1.performAsyncTask();
}
}
(3)分离异步调用
如果 performAsyncTask
不需要立即执行,考虑将它移到另一个服务中,或通过其他机制避免直接的循环依赖。
@Service
public class CDTestsService1Impl implements CDTestsService1 {
@Autowired
private TaskExecutor taskExecutor; // Spring 提供的 TaskExecutor
@Override
public void performAsyncTask() {
taskExecutor.execute(() -> {
System.out.println("performAsyncTask...");
});
}
}
@Service
public class CDTestsService2Impl implements CDTestsService2 {
@Autowired
private CDTestsService1 cdTestsService1;
@Override
public void performTask() {
cdTestsService1.performAsyncTask();
}
}
使用 TaskExecutor
来执行异步任务可以将异步执行的逻辑与服务依赖解耦,避免直接在服务之间形成循环依赖。