Title

@Async配置与使用

应用场景

同步:同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。

异步:异步调用则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。

例如, 在某个调用中,需要顺序调用 A, B, C三个过程方法;如他们都是同步调用,则需要将他们都顺序执行完毕之后,方算作过程执行完毕;如B为一个异步的调用方法,则在执行完A之后,调用B,并不等待B完成,而是执行开始调用C,待C执行完毕之后,就意味着这个过程执行完毕了。

在Java中,一般在处理类似的场景之时,都是基于创建独立的线程去完成相应的异步调用逻辑,通过主线程和不同的业务子线程之间的执行流程,从而在启动独立的线程之后,主线程继续执行而不会产生停滞等待的情况。

Spring 已经实现的线程池

1、 SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
2、 SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
3、 ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
4、 SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
5、 ThreadPoolTaskExecutor :最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

异步的方法

1、 最简单的异步调用,返回值为void
2、 带参数的异步调用,异步方法可以传入参数
3、 存在返回值,常调用返回Future

Spring中启用@Async

// 基于Java配置的启用方式:
@Configuration  
@EnableAsync  
public class ThreadPoolTaskConfig { ... }  
 
// Spring boot启用:
@EnableAsync
@EnableTransactionManagement
@SpringBootApplication
public class MyTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyTestApplication.class, args);
    }
}

@Async应用默认线程池

Spring应用默认的线程池,指在@Async注解在使用时,不指定线程池的名称。查看源码,@Async的默认线程池为SimpleAsyncTaskExecutor

使用默认线程问题:项目上对一些慢接口的优化,把很多接口加上了@Async,上线运行一段时间后,发现线程数量激增!!!

TaskExecutor implementation that fires up a new Thread for each task, executing it asynchronously.

每提交一个任务,就会创建一个线程,能不炸吗?

解决方法: @Async(value="taskExecutor"), value参数传入自定义的线程池。

package com.z.config;

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.ThreadPoolExecutor;

/**
 * 线程池配置
 */
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {

/**
 *   默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,
 *    当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
 *  当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝
 */

    /**
     * 核心线程数(默认线程数)
     */
    private static final int corePoolSize = 20;
    /**
     * 最大线程数
     */
    private static final int maxPoolSize = 150;
    /**
     * 允许线程空闲时间(单位:默认为秒)
     */
    private static final int keepAliveTime = 10;
    /**
     * 缓冲队列大小
     */
    private static final int queueCapacity = 200;
    /**
     * 线程池名前缀
     */
    private static final String threadNamePrefix = "Async-Service-";

    @Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);

        // 线程池对拒绝任务的处理策略
        // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

在线程池应用中,参考阿里巴巴java开发规范

线程池不允许使用Executors去创建,不允许使用系统默认的线程池,推荐通过ThreadPoolExecutor的方式,这样的处理方式让开发的工程师更加明确线程池的运行规则,规避资源耗尽的风险。Executors各个方法的弊端:

new FixedThreadPool和new SingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

@Async调用

注意:如下方式会使@Async失效

  • 异步方法使用static修饰
  • 异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类
  • 异步方法不能与被调用的异步方法在同一个类中
  • 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
  • 如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解

无返回值调用

基于@Async无返回值调用,直接在使用类,使用方法(建议在使用方法)上,加上注解。若需要抛出异常,需手动new一个异常抛出。

/**
  * 带参数的异步调用 异步方法可以传入参数
  *  对于返回值是void,异常会被AsyncUncaughtExceptionHandler处理掉
  * @param s
  */
 @Async("taskExecutor")
 public void asyncInvokeWithException(String s) {
     log.info("asyncInvokeWithParameter, parementer={}", s);
     throw new IllegalArgumentException(s);
 }

有返回值Future调用

/**
 * 异常调用返回Future
 *  对于返回值是Future,不会被AsyncUncaughtExceptionHandler处理,需要我们在方法中捕获异常并处理
 *  或者在调用方在调用Futrue.get时捕获异常进行处理
 * 
 * @param i
 * @return
 */
@Async("taskExecutor")
public Future<String> asyncInvokeReturnFuture(int i) {
    log.info("asyncInvokeReturnFuture, parementer={}", i);
    Future<String> future;
    try {
        Thread.sleep(1000 * 1);
        future = new AsyncResult<String>("success:" + i);
        throw new IllegalArgumentException("a");
    } catch (InterruptedException e) {
        future = new AsyncResult<String>("error");
    } catch(IllegalArgumentException e){
        future = new AsyncResult<String>("error-IllegalArgumentException");
    }
    return future;
}

Future的作用

  • 当做一定运算的时候,运算过程可能比较耗时,有时会去查数据库,或是繁重的计算,比如压缩、加密等,在这种情况下,如果我们一直在原地等待方法返回,显然是不明智的,整体程序的运行效率会大大降低。
  • 我们可以把运算的过程放到子线程去执行,再通过 Future 去控制子线程执行的计算过程,最后获取到计算结果。
  • 这样一来就可以把整个程序的运行效率提高,是一种异步的思想。

同时在JDK 1.8的doc中,对Future的描述如下:

A Future represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation.

大概意思就是Future是一个用于异步计算的接口。

举个例子:

比如去吃早点时,点了包子和凉菜,包子需要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候需要等待4分钟,但是如果你在准备包子的时候,可以同时准备凉菜,这样只需要等待3分钟。
Future就是后面这种执行模式。

Future常用方法

方法名 返回值 入参 备注 总结
cancel boolean (boolean mayInterruptIfRunning) 用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。 也就是说Future提供了三种功能:判断任务是否完成,能够中断任务,能够获取任务执行结果
isCancelled boolean 方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
isDone boolean 方法表示任务是否已经完成,若任务完成,则返回true;
get V 方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
get V (long timeout, TimeUnit unit) 用来获取执行结果,如果在指定时间内,还没获取到结果,抛出一个 TimeoutException 异常

超时问题
假设由于网络原因,有一个任务可能长达 1 分钟都没办法返回结果,那么这个时候,我们的主线程会一直卡着,影响了程序的运行效率。
此时我们就可以用 Future 的带超时参数的get(long timeout, TimeUnit unit)方法来解决这个问题。
这个方法的作用是,如果在限定的时间内没能返回结果的话,那么便会抛出一个 TimeoutException 异常,随后就可以把这个异常捕获住,或者是再往上抛出去,这样就不会一直卡着了。

使用

// 接口
Future<Integer> getUserSum(String userId);

// 实现
@Async("taskExecutor")
@Override
public Future<Integer> getUserSum(String userId) {
    Integer userSum = sysUserMapper.getUserSum(userId);
    return new AsyncResult<Integer>(userSum);
}

// get()调用 异步方法与调用在不同类
Future<Integer> userFu = dataAsyncService.getUserSum(userId);
Integer userSum = userFu.get();

// get(long timeout, TimeUnit unit)调用,用来获取执行结果,在限定的时间内没能返回结果的话,那么便会抛出一个 TimeoutException 异常
Integer userSum = 0;
Future<Integer> userFu = dataAsyncService.getUserSum(userId); // 异步调用
try {
    userSum = userFu.get(60, TimeUnit.SECONDS); // 获取结果
} catch (Exception e) {
    System.out.println("获取超时");
}
  • 提交任务后,会同时获得一个Future对象,然后,在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果,因此使用Integer userSum = dataAsyncService.getUserSum(userId).get(60, TimeUnit.SECONDS);,在创建任务后得到Future对象立即调用get()方法获取结果,就是同步了。
  • 在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果

测试@Async是否生效

调用方法:

public void xxx(String userId){
    // 此处省略若干代码
    System.out.println(1);
    xxxService.xxxAsync(userId);
    System.out.println(2);
}

xxxServiceImpl类:

@Async // 注:一般会放在xxxServiceImpl类中
void xxxAsync(String userId);

被调用方法(xxxService类):

public void xxxAsync(String userId){
    System.out.println(3);
    // 此处省略若干代码
    System.out.println(4);
}

如上所述,如果@Async生效,打印顺序必定是 1/2/3/4 ;反之失效

问题:起新线程调用web接口方法报错:No thread-bound request found: Are you referring to request attributes outsi

起新线程调用同类下的另一个web接口方法,直接使用

new Thread(()->{
 this.funcB();
}).start();

因为funcB也会用到session里的信息,所以会报错拿不到session。

解决方法

在起新线程前,添加子线程共享session即可

 //设置子线程共享
 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
 RequestContextHolder.setRequestAttributes(servletRequestAttributes,true);
 // 异步调用 web 接口
 new Thread(()->{
            this.funcB();
        }).start();

原文章地址:
https://blog.csdn.net/yuechuzhixing/article/details/124775218

posted @ 2023-04-03 17:28  快乐小洋人  阅读(1850)  评论(0编辑  收藏  举报