Java异步注解@Async自定义线程池


一.自定义线程池
1、导入pom

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>


2.创建异步配置类AsyncTaskConfig

/**
 * @author wangli
 * @create 2022-10-15 17:48
 */
@EnableAsync// 支持异步操作
@Configuration
public class AsyncTaskConfig {
 
    @Bean("async-executor-spring")
    public Executor AsyncExecutor(){
        ThreadPoolTaskExecutor  executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(50);
        // 缓存队列
        executor.setQueueCapacity(20);
        // 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(200);
        // 异步方法内部线程名称
        executor.setThreadNamePrefix("async-executor-spring");
 
        /**
         * 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
         * 通常有以下四种策略:
         * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
         * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
         * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
 
    /**
     *  1、corePoolSize:核心线程数
     *         * 核心线程会一直存活,及时没有任务需要执行
     *         * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
     *         * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
     *
     *     2、queueCapacity:任务队列容量(阻塞队列)
     *         * 当核心线程数达到最大时,新任务会放在队列中排队等待执行
     *
     *     3、maxPoolSize:最大线程数
     *         * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
     *         * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
     *
     *     4、 keepAliveTime:线程空闲时间
     *         * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
     *         * 如果allowCoreThreadTimeout=true,则会直到线程数量=0
     *
     *     5、allowCoreThreadTimeout:允许核心线程超时
     *     6、rejectedExecutionHandler:任务拒绝处理器
     *         * 两种情况会拒绝处理任务:
     *             - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
     *             - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
     *         * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
     *         * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
     *             - AbortPolicy 丢弃任务,抛运行时异常
     *             - CallerRunsPolicy 执行任务
     *             - DiscardPolicy 忽视,什么都不会发生
     *             - DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
     *         * 实现RejectedExecutionHandler接口,可自定义处理器
     */
 
    /**
     * com.google.guava中的线程池
     * @return
     */
    @Bean("async-executor-guava")
    public Executor GuavaAsyncExecutor() {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("async-executor-guava").build();
        // 当前可用cpu数
        //最佳线程数可通过计算得出http://ifeve.com/how-to-calculate-threadpool-size/
        int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;
        /**
         * int corePoolSize,
         * int maximumPoolSize,
         * long keepAliveTime,
         * TimeUnit unit,
         * BlockingQueue<Runnable> workQueue,
         * ThreadFactory threadFactory
         */
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100,
                200, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(), threadFactory);
        //允许核心线程超时
        threadPool.allowsCoreThreadTimeOut();
        return threadPool;
    }
 
}


3.创建controller类

/**
 * @author wangli
 * @create 2022-10-15 17:48
 */
@Controller
public class AsyncController {
    @Autowired
    private AsyncService asyncService;
 
    @PostMapping("/AsyncMethond")
    public void AsyncMethond(){
        asyncService.AsyncMethond();
    }
}

4.创建service类

/**
 * @author wangli
 * @create 2022-10-15 17:49
 */
public interface AsyncService {
 
    public void AsyncMethond();
}


5.创建serviceimpl

/**
 * @author wangli
 * @create 2022-10-15 17:49
 */
@Service
public class AsyncServiceImpl implements AsyncService {
    @Override
    @Async("async-executor-guava")
    public void AsyncMethond() {
        System.out.println("调用异步方法");
    }
}

6.启动类:添加@EnableAsync注解

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

二.@Async注解
@Async的作用就是异步处理任务。

在方法上添加@Async,表示此方法是异步方法;
在类上添加@Async,表示类中的所有方法都是异步方法;
使用此注解的类,必须是Spring管理的类;
需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效;
在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池,@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor。
默认线程池的默认配置如下:

默认核心线程数:8;
最大线程数:Integet.MAX_VALUE;
队列使用LinkedBlockingQueue;
容量是:Integet.MAX_VALUE;
空闲线程保留时间:60s;
线程池拒绝策略:AbortPolicy;
从最大线程数可以看出,在并发情况下,会无限制的创建线程

也可以通过yml重新配置:

spring:
  task:
    execution:
      pool:
        max-size: 10
        core-size: 5
        keep-alive: 3s
        queue-capacity: 1000
        thread-name-prefix: my-executor
三.为什么异步会失败
注解@Async的方法不是public方法;
注解@Async的返回值只能为void或Future;
注解@Async方法使用static修饰也会失效;
没加@EnableAsync注解;
调用方和@Async不能在一个类中;
在Async方法上标注@Transactional是没用的,但在Async方法调用的方法上标注@Transcational是有效的;
四.线程池执行流程


 

五.如何合理规划线程池的大小
这个问题虽然看起来很小,却并不那么容易回答。大家如果有更好的方法欢迎赐教,先来一个天真的估算方法:假设要求一个系统的TPS(Transaction Per Second或者Task Per Second)至少为20,然后假设每个Transaction由一个线程完成,继续假设平均每个线程处理一个Transaction的时间为4s。那么问题转化为:

如何设计线程池大小,使得可以在1s内处理完20个Transaction?

计算过程很简单,每个线程的处理能力为0.25TPS,那么要达到20TPS,显然需要20/0.25=80个线程。

很显然这个估算方法很天真,因为它没有考虑到CPU数目。一般服务器的CPU核数为16或者32,如果有80个线程,那么肯定会带来太多不必要的线程上下文切换开销。

再来第二种简单的但不知是否可行的方法(N为CPU总核数):

如果是CPU密集型应用,则线程池大小设置为N+1
如果是IO密集型应用,则线程池大小设置为2N+1
如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。

接下来在这个文档:服务器性能IO优化 中发现一个估算公式:

1    最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

1    最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
可以得出一个结论:

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

上一种估算方法也和这个结论相合。

一个系统最快的部分是CPU,所以决定一个系统吞吐量上限的是CPU。增强CPU处理能力,可以提高系统吞吐量上限。但根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提高系统吞吐量,就需要从“系统短板”(比如网络延迟、IO)着手:

尽量提高短板操作的并行化比率,比如多线程下载技术
增强短板能力,比如用NIO替代IO
第一条可以联系到Amdahl定律,这条定律定义了串行系统并行化后的加速比计算公式:

1    加速比=优化前系统耗时 / 优化后系统耗时
加速比越大,表明系统并行化的优化效果越好。Addahl定律还给出了系统并行度、CPU数目和加速比的关系,加速比为Speedup,系统串行化比率(指串行执行代码所占比率)为F,CPU数目为N:

1    Speedup <= 1 / (F + (1-F)/N)
当N足够大时,串行化比率F越小,加速比Speedup越大。

写到这里,我突然冒出一个问题。

是否使用线程池就一定比使用单线程高效呢?

答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:

多线程带来线程上下文切换开销,单线程就没有这种开销

当然“Redis很快”更本质的原因在于:Redis基本都是内存操作,这种情况下单线程可以很高效地利用CPU。而多线程适用场景一般是:存在相当比例的IO和网络操作。

所以即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值。


原文链接:https://blog.csdn.net/wang20010104/article/details/127337665

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
@Async 是一个注解,用于标记一个方法是异步执行的。它通常与 Spring 框架一起使用,可以实现异步方法的调用和执行。 在 Spring 中,@Async 注解需要与线程池一起使用,以便在方法被调用时,能够将任务提交给线程池进行异步执行。这样可以避免阻塞主线程,提高系统的并发能力和响应速度。 使用 @Async 注解时,需要在 Spring 配置文件中配置一个线程池Spring 提供了一个 TaskExecutor 接口,可以通过实现该接口来创建自定义线程池。同时,Spring 也提供了一些默认的线程池实现,如 ThreadPoolTaskExecutor。 ThreadPoolTaskExecutor 是一个基于线程池的 TaskExecutor 实现类,它可以配置线程池的核心线程数、最大线程数、队列容量等参数。通过配置不同的参数,可以根据实际需求来调整线程池的大小和行为。 使用 @Async 注解时,需要在方法上添加该注解,并指定要使用的线程池。例如: ```java @Async("myThreadPool") public void asyncMethod() { // 异步执行的方法体 } ``` 上述代码中,"myThreadPool" 是配置文件中定义的线程池的名称。 使用 @Async 注解时,需要注意以下几点: 1. 异步方法必须定义在 Spring 管理的 Bean 中。 2. 异步方法不能在同一个类中相互调用,因为 Spring 会通过代理来实现异步调用,同一个类中的方法调用不会触发代理。 3. 异步方法的返回值类型只能是 void 或者 Future。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值