@Async使用

Spring的线程机制

Spring框架本身不管理线程,它依赖于运行Spring应用程序的服务器来处理并发。

在典型的Spring Web应用程序中,当一个HTTP请求到达服务器时,服务器(如Tomcat)将从其线程池中选取一个线程来处理这个请求。这个线程将负责执行整个请求的处理流程,包括调用Spring的Controller方法、进行业务逻辑处理、访问数据库等。在此过程中,Spring并不进行任何线程管理或调度。

在整个处理流程完成后,服务器将处理结果返回给客户端,然后线程返回到服务器的线程池中,等待处理下一个请求。

因此,在不进行任何线程配置的情况下,Spring应用程序的并发处理能力主要取决于运行该应用程序的服务器的线程池配置,包括线程池的大小、线程调度策略等。

需要注意的是,即使Spring不管理线程,也需要注意线程安全问题。例如,如果Spring Bean被多个线程共享,且包含可变状态,则需要保证其线程安全。另一方面,Spring提供了一些机制(如@Async注解、TaskExecutor接口等)来帮助进行显式的并发处理。

@Async

概念原理

@Async注解是Spring框架提供的一种异步调用机制

它的实现原理如下

  1. 在Spring容器启动时,会扫描所有的@Service、@Controller、@Component等注解标注的Bean,查找其中的@Async注解。
  2. 当发现一个方法上标注了@Async注解时,Spring会使用CGLIB动态代理机制为该方法生成一个代理对象。
  3. 代理对象会将异步方法的调用转换为一个Runnable任务,并提交给线程池执行。
  4. 当任务执行完成后,代理对象会将异步方法的返回值或异常信息封装到一个Future对象中,供调用方使用。

具体来说,@Async注解的实现依赖于以下几个组件:

  1. TaskExecutor:用来执行异步任务的线程池。默认使用SimpleAsyncTaskExecutor,一个没有最大数量限制的线程池,每次调用都会创建一个新的线程,并发大的时候会产生严重的性能问题。
  2. AsyncAnnotationBeanPostProcessor:用来扫描Bean中的@Async注解,并为标注了该注解的方法生成代理对象。
  3. AsyncUncaughtExceptionHandler:用来处理异步任务执行中出现的异常。可以自定义实现该接口来定制异常处理逻辑。

总体来说,@Async注解的实现原理主要是基于动态代理线程池机制来实现的。它可以大大简化异步调用的代码编写,提高应用程序的并发性能和资源利用率。

使用

@AsyncSpring内置注解,用来处理异步任务,在SpringBoot中同样适用,且在SpringBoot项目中,除了boot本身的starter外,不需要额外引入依赖。

而要使用@Async,需要在 主启动类或配置类上加上@EnableAsync主动声明来开启异步方法。

@EnableAsync
@SpringBootApplication
public class Application {
    //...
}

或者

@EnableAsync
@Configuration
public class config {
    //...
}

在方法或类上加@Async,方法上加就是这个方法开启异步,类上就是这个类所有方法都异步

@Component
public class MyAsyncTask {
     
    @Async
    public void asyncCpsItemImportTask(Long platformId, String jsonList){
        //...具体业务逻辑
    }
}

代码示例

  1. 在配置类中配置线程池并开启异步注解:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "asyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 核心线程数
        executor.setMaxPoolSize(20); // 最大线程数
        executor.setQueueCapacity(100); // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setThreadNamePrefix("AsyncThread-"); // 线程名称前缀
        executor.initialize();//初始化线程池
        return executor;
    }
}
  1. 在需要异步执行的方法上使用@Async注解,并指定使用的线程池:
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class MyService {
    
    @Async("asyncExecutor")
    public void doSomethingAsync() {
        // 异步执行的方法体
    }
}

在上述示例中,通过在配置类中创建名为"asyncExecutor"的ThreadPoolTaskExecutor Bean,并使用@EnableAsync注解启用异步支持。

然后,在需要异步执行的方法上使用@Async注解,并指定使用的线程池名为"asyncExecutor"。

通过这样的配置,就可以在Spring Boot项目中使用ThreadPoolExecutor线程池进行异步处理。可以根据实际需求调整线程池的核心线程数、最大线程数、队列容量等参数,以满足项目的并发需求。

失效情况

在以下情况下,@Async注解可能会失效:

  1. 异步方法与调用方法在同一个类中:Spring通过基于代理的方式实现@Async注解的功能,而基于代理的方式只能在不同的类之间才能生效。如果异步方法和调用方法在同一个类中,Spring无法通过代理来增强方法,因此@Async注解会失效。
  2. 异步方法没有被Spring容器管理:异步方法需要被Spring容器管理,才能使@Async注解生效。如果异步方法没有被纳入Spring容器中,那么@Async注解将不会生效。比如spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解
  3. 异步方法使用了final方法:如果异步方法使用了final修饰符,那么@Async注解将会失效。jdk动态代理要求有接口,通过重写方法进行代理增强;而cglib通过继承目标类进行代理增强,但final修饰方法无法被重写和继承,所以失效。
  4. 异步方法不是public方法:注解@Async的方法不是public方法
  5. 异步方法的返回值不是void或者Future:注解@Async的返回值只能为void或者Future
  6. 异步方法是static的:注解@Async方法使用static修饰也会失效
  7. 异步方法没有正确配置线程池:在使用@Async注解时,需要配置线程池,如果没有正确配置线程池,那么异步方法将无法执行。

需要注意的是,以上情况下@Async注解失效的原因并不是@Async本身的问题,而是因为某些限制条件或配置错误导致无法实现异步调用。确保异步方法在不同类中被调用、被Spring容器管理,并且正确配置线程池,通常可以避免@Async失效的问题。

效果展示

无返回值方法异步

一个请求内部并行执行两个任务

如下,一个/user请求到来,两个ttl1()并行执行,而不是等上面的结束再执行下一个

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "asyncExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }
}

Controller

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
    private TtlTool ttlTool;

    //localhost:8080/user/ttl
    @RequestMapping("/ttl")
    public void ttl() throws InterruptedException{

        ttlTool.ttl1();
        ttlTool.ttl1();

    }

}

异步方法

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class TtlTool {

    //volatile的作用是: 线程对副本变量进行修改后,其他线程能够立刻同步刷新最新的数值。这个就是可见性.
    private volatile Integer i = 0;

    @Async("asyncExecutor")
    public void ttl1() throws InterruptedException{

        this.i = i+1;
        int j = this.i;

        System.out.println("第"+i+"个异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        Thread.sleep(3000);

        System.out.println("第"+j+"个异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

    }
}

执行结果

使用了异步@Async的执行结果:

第1个异步方法被  AsyncThread-1  开始执行于 2023-09-23 10:17:04
第2个异步方法被  AsyncThread-2  开始执行于 2023-09-23 10:17:04

第1个异步方法被  AsyncThread-1  结束执行于2023-09-23 10:17:07
第2个异步方法被  AsyncThread-2  结束执行于2023-09-23 10:17:07

如果去掉异步执行,将TtlTool@Async("asyncExecutor")注解去掉,执行结果就是

第1个异步方法被  http-nio-8080-exec-2  开始执行于 2023-09-23 10:25:05
第1个异步方法被  http-nio-8080-exec-2  结束执行于2023-09-23 10:25:08

第2个异步方法被  http-nio-8080-exec-2  开始执行于 2023-09-23 10:25:08
第2个异步方法被  http-nio-8080-exec-2  结束执行于2023-09-23 10:25:11

可见在使用了异步@Async之后,第1个与第2个异步任务都是在10:17:04同一时间执行的,而不是等3秒在执行,证明异步执行成功

带返回值的方法异步

需要使用回调函数,将异步结果返回

返回值类

public class Demo {

    private String one;
    private String two;
    private String three;
    private String four;

    public String getOne() {
        return one;
    }

    public void setOne(String one) {
        this.one = one;
    }

    public String getTwo() {
        return two;
    }

    public void setTwo(String two) {
        this.two = two;
    }

    public String getThree() {
        return three;
    }

    public void setThree(String three) {
        this.three = three;
    }

    public String getFour() {
        return four;
    }

    public void setFour(String four) {
        this.four = four;
    }
}

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "asyncExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }

}

异步方法

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;

@Component
public class TtlTool {

    @Async("asyncExecutor")
    public CompletableFuture<Demo> ttl1() throws InterruptedException{

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        Thread.sleep(5000);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        CompletableFuture<Demo> cd = CompletableFuture.completedFuture(d);

        return cd;
    }
}

Controller

import com.example.bean.Demo;
import com.example.service.TtlTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;


@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;

    @RequestMapping("/user/{name}")
    public DeferredResult<Demo> demo(@PathVariable String name) throws InterruptedException, ExecutionException {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        //用于封装异步返回值给前端
        DeferredResult<Demo> deferredResult = new DeferredResult<>();

        CompletableFuture<Demo> future = null;
        try {
            future = ttlTool.ttl1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /**
         * thenApply 表示某个任务执行完成后执行的动作,即回调方法:
         *  会在ttlTool.ttl1()的方法执行结束后,thenApply才会触发,与ttl1()使用同一个线程。
         *	如果用thenAcceptAsync,则会用新线程进行回调处理
         * */
        future.thenAccept(result -> {
            // 在异步方法执行完成后,使用回调函数处理结果
            System.out.println("回调方法被  "+Thread.currentThread().getName()+"  执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            deferredResult.setResult(result); // 设置返回结果
        });

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        System.out.println();

        return deferredResult;
    }

}

执行结果

同时发送两次"http://localhost:8081/user/asd"请求,结果如下:

Controller方法被  http-nio-8081-exec-1  开始执行于 2023-10-20 10:07:58
Controller方法被  http-nio-8081-exec-1  结束执行于 2023-10-20 10:07:58

异步方法被  WebmvcThread-1  开始执行于 2023-10-20 10:07:58

Controller方法被  http-nio-8081-exec-9  开始执行于 2023-10-20 10:07:58
Controller方法被  http-nio-8081-exec-9  结束执行于 2023-10-20 10:07:58

异步方法被  WebmvcThread-2  开始执行于 2023-10-20 10:07:58

异步方法被  WebmvcThread-1  结束执行于2023-10-20 10:08:03

回调方法被  WebmvcThread-1  执行于 2023-10-20 10:08:03

异步方法被  WebmvcThread-2  结束执行于2023-10-20 10:08:03

回调方法被  WebmvcThread-2  执行于 2023-10-20 10:08:03
  • http-nio-8081-exec-1http-nio-8081-exec-9tomcat同时开启的两个线程,用于处理两个同时到来的/user/asd请求。证明请求是异步执行的
  • WebmvcThread-1是被http-nio-8081-exec-1所调用的执行异步方法ttl1()的线程,WebmvcThread-2是被http-nio-8081-exec-9所调用的执行异步方法ttl1()的线程。WebmvcThread-1WebmvcThread-2的开始执行时间一致,证明ttl()方法是异步执行
  • WebmvcThread-1WebmvcThread-2的结束执行时间是在开始时间的5秒后,对应异步方法中的休眠5秒。
  • 由于回调方法thenAcceptWebmvcThread-1WebmvcThread-2执行结束后没有被释放,而是将结果返回。证明异步回调成功

请求异步

有返回值,同一个url,多次请求的异步执行

默认情况下,请求的异步是tomcat自动实现的,tomcat中自带线程池,当多个一摸一样的请求到来时,tomcat会启动多个线程同时处理,这里使用@Async来做演示

可以在yamltomcat的配置:

server:
  port: 8081
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # 连接数满后的排队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
    #最大连接数
    max-connections: 100

如下,多个/user请求同时到来,Controller异步进行处理,并返回结果给浏览器

返回值类型

public class Demo {

    private String one;
    private String two;
    private String three;
    private String four;

    public String getOne() {
        return one;
    }

    public void setOne(String one) {
        this.one = one;
    }

    public String getTwo() {
        return two;
    }

    public void setTwo(String two) {
        this.two = two;
    }

    public String getThree() {
        return three;
    }

    public void setThree(String three) {
        this.three = three;
    }

    public String getFour() {
        return four;
    }

    public void setFour(String four) {
        this.four = four;
    }
}

配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "asyncExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(100);
        // 队列容量,如果此数字大于0,使用队列LinkedBlockingQueue
        executor.setQueueCapacity(100);
        // 线程名称前缀
        executor.setThreadNamePrefix("WebmvcThread-");
        //初始化
        executor.initialize();

        return executor;
    }

}

异步方法

@Component
public class TtlTool {


    @Async("asyncExecutor")
    public CompletableFuture<Demo> ttl1() throws InterruptedException{


        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        Thread.sleep(3000);
        Demo d = new Demo();
        d.setOne("1");
        d.setTwo("2");
        d.setThree("3");
        d.setFour("4");

        System.out.println("异步方法被  "+Thread.currentThread().getName()+"  结束执行于"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        CompletableFuture<Demo> cd = CompletableFuture.completedFuture(d);

        return cd;
    }
}

默认的Controller

使用tomcat的线程进行异步处理,controller请求会被tomcat线程池中的线程处理,而异步方法ttl1()会被我们配置的@Async线程池处理

@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;
    
    @RequestMapping("/user/{name}")
    public DeferredResult<Demo> demo(@PathVariable String name) throws InterruptedException, ExecutionException {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        DeferredResult<Demo> deferredResult = new DeferredResult<>();

        CompletableFuture<Demo> future = null;
        try {
            future = ttlTool.ttl1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        future.thenAccept(new Consumer<Demo>() {
            @Override
            public void accept(Demo result) {
                // 在异步方法执行完成后,使用回调函数处理结果
                System.out.println("Async Result: " + result);
                deferredResult.setResult(result); // 设置返回结果
            }
        });

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        System.out.println();

        return deferredResult;
    }

}

使用@Async的Controller

实际开发并不需要使用这种配置,只是演示一下,这样也能用,但是没法将数据返回前端。

原因是:线程切换了,请求的处理被tomcat线程交给了我们配置的@Async线程,tomcat线程转而被释放,当@Async线程处理完请求后,返回值无法还给原来调用的tomcat线程了

@RestController
public class IndexController {

    @Autowired
    private TtlTool ttlTool;

    @RequestMapping("/user/{name}")
    @Async
    public DeferredResult<Demo> demo(@PathVariable String name) throws InterruptedException, ExecutionException {

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  开始执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        DeferredResult<Demo> deferredResult = new DeferredResult<>();

        CompletableFuture<Demo> future = null;
        try {
            future = ttlTool.ttl1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        future.thenAccept(new Consumer<Demo>() {
            @Override
            public void accept(Demo result) {
                // 在异步方法执行完成后,使用回调函数处理结果
                System.out.println("Async Result: " + result);
                deferredResult.setResult(result); // 设置返回结果
            }
        });

        System.out.println("Controller方法被  "+Thread.currentThread().getName()+"  结束执行于 "+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        System.out.println();

        return deferredResult;
    }

}

执行结果

**注意一个问题:**在开启了Controller异步后,在某些浏览器上对该Controller,进行同一时间的多次请求时,可能还是顺序执行,没有达到异步效果,这不是代码问题,而是浏览器原因,可以使用postman或多个不同浏览器测试

默认tomcat配置下

同时发送两次"http://localhost:8081/user/asd"请求,结果如下:

Controller方法被  http-nio-8081-exec-4  开始执行于 2023-10-20 09:29:02
Controller方法被  http-nio-8081-exec-4  结束执行于 2023-10-20 09:29:02

异步方法被  WebmvcThread-3  开始执行于 2023-10-20 09:29:02

Controller方法被  http-nio-8081-exec-3  开始执行于 2023-10-20 09:29:02
Controller方法被  http-nio-8081-exec-3  结束执行于 2023-10-20 09:29:02

异步方法被  WebmvcThread-4  开始执行于 2023-10-20 09:29:02
异步方法被  WebmvcThread-3  结束执行于2023-10-20 09:29:07
Async Result: Demo{one='1', two='2', three='3', four='4'}

异步方法被  WebmvcThread-4  结束执行于2023-10-20 09:29:07
Async Result: Demo{one='1', two='2', three='3', four='4'}

http-nio-端口号-exec-数字便是tomcat自带线程

两次请求进入,可以看到http-nio-8081-exec-3http-nio-8081-exec-4是同一时间执行的,这是tomcat默认配置的异步处理。

紧接着能看到,异步方法用我们自己配置的线程池执行了,WebmvcThread-3WebmvcThread-4也是在同一时间执行,异步成功

@Async配置下

同时发送两次"http://localhost:8081/user/asd"请求,结果如下:

Controller方法被  WebmvcThread-1  开始执行于 2023-10-20 09:45:06
Controller方法被  WebmvcThread-1  结束执行于 2023-10-20 09:45:06

异步方法被  WebmvcThread-2  开始执行于 2023-10-20 09:45:06

Controller方法被  WebmvcThread-3  开始执行于 2023-10-20 09:45:07
Controller方法被  WebmvcThread-3  结束执行于 2023-10-20 09:45:07

异步方法被  WebmvcThread-4  开始执行于 2023-10-20 09:45:07
异步方法被  WebmvcThread-2  结束执行于2023-10-20 09:45:11
Async Result: Demo{one='1', two='2', three='3', four='4'}

异步方法被  WebmvcThread-4  结束执行于2023-10-20 09:45:12
Async Result: Demo{one='1', two='2', three='3', four='4'}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值