一文带你吃透@Async,让异步编程so easy!

一文带你吃透@Async,让异步编程so easy!

异步编程知多少

在编程的世界里,我们常常会遇到这样的场景:程序需要执行一些耗时的操作,比如读取大文件、进行复杂的数据库查询或者发起网络请求。在传统的同步编程模式下,当程序执行到这些耗时操作时,它会乖乖地停下来,一直等到操作完成,才会继续执行后续的代码。这就好比你去餐厅吃饭,点完菜后,服务员就一直站在那里等厨师把菜做好,才去做其他事情,期间什么都不做,这显然效率低下。

同步编程在处理这类耗时任务时,会导致线程阻塞,使得程序的响应速度变慢。特别是在高并发的场景下,大量的线程都在等待耗时操作完成,会严重消耗系统资源,导致系统性能急剧下降。

而异步编程的出现,就像是给程序注入了一剂 "强心针"。它允许程序在执行耗时操作时,不阻塞当前线程,而是继续执行其他任务。当耗时操作完成后,再通过特定的机制通知程序来处理结果。就好像餐厅引入了叫号系统,你点完菜后,服务员不用一直等着,而是可以去服务其他顾客,等你的菜做好了,再通过叫号通知你。这样一来,程序的性能和响应速度得到了极大的提升,能够更高效地处理多个任务,提高了系统的吞吐量和并发能力。

在 Java 开发中,Spring 框架为我们提供了一个非常强大的实现异步编程的工具 ------@Async 注解。它就像是一把神奇的钥匙,能够轻松地将普通方法转变为异步执行的方法,让我们的程序具备更强大的处理能力。接下来,就让我们一起深入了解 @Async 注解的奥秘吧!

@Async 注解大揭秘

(一)@Async 是什么

@Async 是 Spring 框架提供的一个注解,它就像是一个神奇的 "任务派遣员",当你在方法上标记了 @Async 注解,就相当于给这个方法贴上了 "异步执行" 的标签,Spring 框架会在幕后为你创建一个新的线程,让这个方法在新线程中独立执行 ,而不是在调用它的主线程中执行。这样一来,主线程就不会被方法的执行过程阻塞,可以继续去处理其他任务,大大提高了程序的执行效率。

(二)@Async 能解决什么问题

在实际开发中,@Async 注解有着广泛的应用场景,能够帮我们解决许多实际问题。例如,在一个电商系统中,当用户完成订单支付后,系统需要发送一封包含订单详情的邮件给用户,同时记录支付日志。发送邮件和记录日志这两个操作都需要一定的时间,如果采用同步执行的方式,用户在支付完成后,需要等待邮件发送和日志记录完成后才能看到支付成功的页面,这无疑会大大降低用户体验。

而使用 @Async 注解,我们可以将发送邮件和记录日志的方法标记为异步执行。这样,当用户支付完成后,主线程会立即返回支付成功的响应给用户,而发送邮件和记录日志的操作则会在后台线程中异步执行,用户无需等待这些操作完成,大大提高了系统的响应速度和用户体验。

再比如,在一个数据处理系统中,需要定期从数据库中读取大量数据进行复杂的计算和分析。这个数据处理过程可能会非常耗时,如果在主线程中同步执行,会导致系统在处理数据期间无法响应其他请求。通过 @Async 注解,将数据处理方法设置为异步执行,主线程就可以继续处理其他请求,提高了系统的并发处理能力,使得系统能够在处理大数据量的同时,还能保持良好的响应性能。

@Async 快速上手

(一)准备工作

在使用 @Async 注解之前,我们需要先在 Spring Boot 项目中引入相关依赖。如果你使用的是 Maven 项目,只需要在pom.xml文件中添加 Spring AOP 的依赖即可,因为 @Async 注解是基于 Spring AOP 实现的,而 Spring Boot Web Starter 中已经包含了 Spring AOP 的依赖,所以如果你的项目中已经引入了 Spring Boot Web Starter ,则无需额外引入。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

如果你使用的是 Gradle 项目,则在build.gradle文件中添加以下依赖:

groovy 复制代码
implementation 'org.springframework.boot:spring-boot-starter-aop'

添加完依赖后,还需要在 Spring Boot 的启动类或者配置类上添加@EnableAsync注解,来开启 Spring 的异步功能。例如,在启动类AsyncApplication上添加@EnableAsync注解:

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class AsyncApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncApplication.class, args);
    }
}

这样,Spring 容器在启动时就会扫描并识别被@Async注解标记的方法,并为其创建异步执行的代理对象。

(二)简单示例

接下来,我们通过一个简单的示例来看看 @Async 注解的实际使用。假设我们有一个服务类AsyncService,其中包含一个需要异步执行的方法asyncMethod

首先,创建AsyncService类,并在asyncMethod方法上添加@Async注解:

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {
    @Async
    public void asyncMethod() {
        System.out.println("异步方法执行,线程:" + Thread.currentThread().getName());
        // 模拟一些耗时操作,比如线程睡眠3秒
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("异步方法执行完成");
    }
}

然后,创建一个控制器类AsyncController,用于调用AsyncService中的异步方法:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AsyncController {
    @Autowired
    private AsyncService asyncService;

    @GetMapping("/async/call")
    public String callAsync() {
        asyncService.asyncMethod();
        return "异步方法已调用!";
    }
}

当我们访问/async/call接口时,asyncMethod方法会在一个新的线程中异步执行,而主线程会立即返回 "异步方法已调用!" 的响应,不会等待asyncMethod方法执行完成。在控制台中,我们可以看到类似以下的输出:

Plain 复制代码
异步方法已调用!
异步方法执行,线程:task-1
异步方法执行完成

可以看到,"异步方法已调用!" 先被输出,说明主线程没有被阻塞,而 "异步方法执行,线程:task-1" 和 "异步方法执行完成" 是在异步线程中输出的,其中 "task-1" 是默认线程池为异步任务分配的线程名称。

(三)自定义线程池

在上面的示例中,我们使用的是 Spring 默认的线程池SimpleAsyncTaskExecutor,它在每次执行异步任务时都会创建一个新的线程,而不会复用已有的线程。这种线程池在高并发场景下会带来很大的性能开销,因为频繁创建和销毁线程会消耗大量的系统资源,容易导致系统性能下降 ,甚至可能引发OutOfMemoryError错误。

为了避免这些问题,我们可以自定义线程池,通过配置线程池的参数,如核心线程数、最大线程数、队列容量等,来优化异步任务的执行性能。在 Spring Boot 中,我们可以通过创建一个配置类来定义自定义线程池。

创建一个线程池配置类AsyncConfig,代码如下:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class AsyncConfig {
    @Bean(name = "customThreadPool")
    public Executor customThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数,线程池创建时初始化的线程数
        executor.setCorePoolSize(5);
        // 设置最大线程数,线程池能够创建的最大线程数
        executor.setMaxPoolSize(10);
        // 设置队列容量,任务队列的大小
        executor.setQueueCapacity(20);
        // 设置线程的存活时间,当线程空闲时间超过该值时,线程会被销毁
        executor.setKeepAliveSeconds(60);
        // 设置线程名称前缀,方便在日志中识别线程所属的线程池
        executor.setThreadNamePrefix("custom-async-");
        // 设置拒绝策略,当任务队列已满且线程数达到最大线程数时,新任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolTaskExecutor.CallerRunsPolicy());
        // 初始化线程池
        executor.initialize();
        return executor;
    }
}

在上述配置中:

  • corePoolSize设置为 5,表示线程池初始化时会创建 5 个核心线程,这些线程会一直存活,即使它们处于空闲状态。

  • maxPoolSize设置为 10,表示线程池最多可以创建 10 个线程,当任务队列已满且核心线程都在忙碌时,会创建新的线程来处理任务,但线程总数不会超过 10 个。

  • queueCapacity设置为 20,表示任务队列的容量为 20,当核心线程都在忙碌时,新的任务会被放入队列中等待执行。

  • keepAliveSeconds设置为 60,表示当线程空闲时间超过 60 秒时,非核心线程会被销毁,以释放系统资源。

  • threadNamePrefix设置为 "custom-async-",这样在日志中可以通过线程名称很容易地识别出这些线程是属于我们自定义线程池的。

  • CallerRunsPolicy拒绝策略表示当任务队列已满且线程数达到最大线程数时,新的任务会由调用者线程(即提交任务的线程)来执行,这样可以避免任务被丢弃,但可能会影响调用者线程的性能,所以需要根据实际业务场景谨慎选择拒绝策略。

配置好自定义线程池后,我们可以在异步方法上指定使用这个线程池。修改AsyncService类中的asyncMethod方法,指定使用customThreadPool线程池:

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {
    @Async("customThreadPool")
    public void asyncMethod() {
        System.out.println("异步方法执行,线程:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("异步方法执行完成");
    }
}

此时,当我们再次访问/async/call接口时,asyncMethod方法会在我们自定义的线程池中执行,在控制台中可以看到线程名称前缀为 "custom-async-",表明自定义线程池配置生效。通过自定义线程池,我们可以根据业务需求灵活调整线程池的参数,提高异步任务的执行效率和系统的稳定性。

注意事项与常见问题

(一)@Async 失效场景

在使用 @Async 注解的过程中,有些小伙伴可能会遇到看似添加了注解,但方法却没有异步执行的情况。这往往是因为陷入了一些 @Async 失效的场景。

  • 未启用 @EnableAsync:这是最常见的原因之一。如果在项目的启动类或配置类上没有添加 @EnableAsync 注解,就相当于没有打开异步功能的开关,@Async 注解自然不会生效。就好比你买了一台新电器,却没有插上电源,它肯定无法工作。

  • 方法不是 public:@Async 注解只能用于修饰 public 方法,如果将其用于非 public 方法,如 private 方法,由于 Spring 的 AOP 机制无法为非 public 方法创建代理,所以异步功能会失效。这就像是给一扇没有对外通道的房间门安装了高级门锁,外人根本无法通过这扇门进入房间。

  • 内部方法调用 :在同一个类中,直接调用被 @Async 注解修饰的方法,会导致异步失效。因为这种情况下,方法调用是通过对象自身(this)进行的,而不是通过 Spring 生成的代理对象,绕过了 AOP 代理机制。例如,在UserService类中,test方法内部调用async方法,async方法的异步功能就不会生效。这就好比你在自己家里叫自己的小名,和别人在外面叫你的大名,效果是不一样的。

java 复制代码
@Service
public class UserService {
    public void test() {
        async("test");
    }
    @Async
    public void async(String value) {
        System.out.println("async:" + value);
    }
}
  • 方法返回值错误 :@Async 注解的异步方法的返回值要么是void,要么是Future及其子类,如CompletableFuture。如果返回值类型不符合要求,也会导致异步功能失效。例如,返回一个普通的String类型,就会出现问题。这就好比你开着一辆只能加柴油的车,却给它加了汽油,车肯定无法正常行驶。

(二)异常处理

在异步方法中,异常处理是一个需要特别注意的点。由于异步方法是在另一个线程中执行的,所以不能像同步方法那样直接在调用处捕获异常。

对于没有返回值的异步方法,如果方法内部抛出异常,默认情况下,这个异常会被SimpleAsyncUncaughtExceptionHandler捕获并打印到控制台,但不会影响主线程的执行。如果我们想要自定义异常处理逻辑,可以实现AsyncUncaughtExceptionHandler接口,并在配置类中通过@Override重写getAsyncUncaughtExceptionHandler方法来指定自定义的异常处理器。例如:

java 复制代码
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.lang.reflect.Method;
import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(20);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.err.println("Exception message - " + throwable.getMessage());
        System.err.println("Method name - " + method.getName());
        for (Object param : obj) {
            System.err.println("Parameter value - " + param);
        }
    }
}

在上述代码中,CustomAsyncExceptionHandler类实现了AsyncUncaughtExceptionHandler接口,并重写了handleUncaughtException方法,在这个方法中,我们可以对异步方法抛出的异常进行自定义处理,比如记录详细的异常日志、发送异常通知等。

对于有返回值的异步方法,通常会返回一个Future对象。我们可以通过Futureget方法来获取异步任务的执行结果,同时,get方法会抛出ExecutionExceptionInterruptedException异常,我们可以在调用get方法的地方捕获这些异常,从而处理异步方法中的异常情况。例如:

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;

import java.util.concurrent.Future;

@Service
public class AsyncService {

    @Async
    public Future<String> asyncMethodWithReturn() {
        try {
            Thread.sleep(3000);
            return new AsyncResult<>("异步方法执行成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
            return new AsyncResult<>("异步方法执行失败");
        }
    }
}

在控制器中调用该方法并处理异常:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

@RestController
public class AsyncController {

    @Autowired
    private AsyncService asyncService;

    @GetMapping("/async/return")
    public String callAsyncWithReturn() {
        try {
            Future<String> future = asyncService.asyncMethodWithReturn();
            return future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            return "异步方法执行出现异常";
        }
    }
}

在这个例子中,asyncMethodWithReturn方法返回一个Future<String>对象,在callAsyncWithReturn方法中,通过future.get()获取异步方法的执行结果,并在catch块中处理可能出现的异常。

(三)事务问题

当 @Async 注解与 @Transactional 注解一起使用时,需要特别注意事务问题。默认情况下,@Async 方法是在新的线程中执行的,它不会继承调用者的事务上下文,这意味着在 @Async 方法内部进行的数据库操作,即使出现异常,也不会回滚调用者所在事务中的操作。例如,在一个电商订单处理系统中,创建订单的方法和发送订单确认邮件的方法分别被 @Transactional 和 @Async 注解修饰,如果发送邮件时出现异常,订单创建的事务不会因为邮件发送失败而回滚,可能会导致数据不一致的问题。

为了解决这个问题,可以通过配置事务传播行为来确保事务的一致性。例如,可以将 @Async 方法的事务传播行为设置为Propagation.REQUIRES_NEW,这样,@Async 方法会创建一个新的事务,如果出现异常,只会回滚这个新事务中的操作,而不会影响调用者的事务。配置方式如下:

java 复制代码
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void createOrder() {
        // 创建订单的逻辑
        asyncSendEmail();
    }

    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void asyncSendEmail() {
        // 发送邮件的逻辑
    }
}

在上述代码中,createOrder方法使用Propagation.REQUIRED传播行为,确保在已有事务中执行;asyncSendEmail方法使用Propagation.REQUIRES_NEW传播行为,确保在新的事务中执行。这样,即使asyncSendEmail方法出现异常,也不会影响createOrder方法中的事务。同时,还需要注意事务管理器的配置,确保其能够正确管理异步方法中的事务。

总结与展望

@Async 注解作为 Spring 框架中实现异步编程的利器,为我们提升系统性能和响应速度提供了便捷的方式。通过本文的介绍,我们了解了异步编程的重要性,深入剖析了 @Async 注解的原理、使用方法、自定义线程池配置,以及在使用过程中需要注意的事项和常见问题的解决方法。

在实际项目中,合理运用 @Async 注解能够显著提高系统的并发处理能力,优化用户体验。例如,在电商、金融、社交等各类高并发应用场景中,将耗时操作异步化,可以让系统在处理大量请求时保持高效稳定运行 。

希望大家在今后的项目开发中,能够充分发挥 @Async 注解的优势,根据具体业务需求灵活配置线程池,妥善处理异常和事务问题,让我们的系统性能更上一层楼。同时,也期待大家在实践中积累更多经验,欢迎在留言区分享你的使用心得和遇到的有趣问题,让我们共同学习,共同进步!

相关推荐
元亨利贞4482 小时前
C#中空值校验情况说明
后端
shark_chili2 小时前
Spring AI alibaba最佳实践-jvm监控诊断agent开发教程
后端
颜酱2 小时前
从0到1实现LRU缓存:思路拆解+代码落地
javascript·后端·算法
IT_陈寒2 小时前
JavaScript这5个隐藏技巧,90%的开发者都不知道!
前端·人工智能·后端
JaguarJack3 小时前
PHP 的异步编程 该怎么选择
后端·php·服务端
风象南3 小时前
AI 写代码效果差?大多数人第一步就错了
人工智能·后端
BingoGo3 小时前
PHP 的异步编程 该怎么选择
后端·php
焗猪扒饭13 小时前
redis stream用作消息队列极速入门
redis·后端·go
树獭非懒13 小时前
AI大模型小白手册|Embedding 与向量数据库
后端·python·llm