springboot线程、线程池

本文详细探讨了在SpringBoot项目中使用定时任务和线程池的实践。通过示例展示了多任务执行的串行与并行情况,分析了任务执行顺序、阻塞影响以及自定义线程池的配置。还讨论了线程池的拒绝策略,包括默认策略和自定义策略的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文摘自:https://dalin.blog.youkuaiyun.com/article/details/115649855

项目中最近使用了多个定时任务处理业务需求,于是在实现业务逻辑过程中,产生了上图一些思考和疑问,现在利用空余时间进行一次复盘。

项目搭建

项目搭建环境:JDK1.8+SpringBoot

主启动类:加上@EnableScheduling

新建定时任务配置类:ScheduledTask;定义两个定时任务,简单打印一下线程名字和时间戳

源码如下:

@Component
public class ScheduledTask {
 
    @Scheduled(cron = "0/1 * * * * ?")
    public void scheduledTask1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
    }
 
    @Scheduled(cron = "0/1 * * * * ?")
    public void scheduledTask2() {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask2 " + System.currentTimeMillis());
    }
}

一、多任务串行执行

1.相同定时任务

先解决多任务定时相同时间,是否存在优先级执行顺序,执行上面的代码,打印日志如下图:

从控制台日志发现,两个定时任务并没有存在一定的执行顺序,存在乱序现象。

故:串行定时任务,没有明显的优先级关系。

2.一个定时任务阻塞

为了实现此场景的条件,将定时任务1中添加死循环逻辑。源码改动如下:

 @Scheduled(cron = "0/1 * * * * ?")
    public void scheduledTask1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
        while (true) {
            Thread.sleep(5000);
        }
    }

从控制台可以得出:多个定时任务时串行执行的,如果一个任务出现阻塞,其他的任务都会受到影响。

二、多任务并行执行

如果要实现并行执行,启动类需要在上面的基础上新增注解@EnableAsync。任务方法上新增@Async注解。

源码如下:

@Component
public class ScheduledTask {
 
    @Scheduled(cron = "0/1 * * * * ?")
    @Async
    public void scheduledTask1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
    }
 
    @Scheduled(cron = "0/1 * * * * ?")
    @Async
    public void scheduledTask2() {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask2 " + System.currentTimeMillis());
    }
}

执行结果如下图:

从控制台中打印的线程名发现:每次执行任务时,都是创建新的线程执行,使用默认线程池SimpleAsyncTaskExecutor。

默认情况下异步调用使用的线程池是SimpleAsyncTaskExecutor,该线程池是不被推荐,因为该线程池的线程不重用,每次调用都会创建一个新的线程。所以需要我们自定义线程池。

自定义线程池

1.自定义局部线程池

局部线程池实际上就是指异步方法上需要指定使用该线程池,否则将使用默认线程池。

配置异步线程池源码如下:

@Component
public class AsyncTaskExecutorConfig {
 
    /**
     * 重写AsyncTaskExecutor对象,实现全局异步线程,即@Async注解需指定线程池
     */
 
   @Bean(value = "asyncTaskExecutor")
    public AsyncTaskExecutor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("defineAsyncTask-");
        executor.setMaxPoolSize(10);
        executor.setCorePoolSize(3);
        executor.setQueueCapacity(100);
           /*
            线程池对拒绝任务的处理策略(rejection policy):
            当线程池已经达到最大线程数量,没有空闲线程时,新任务该如何处理
            可选策略:
            CallerRunsPolicy:当线程池没有能力处理时直接在执行方法的调用线程中运行被拒绝的任务
            如果执行程序已经关闭,将丢弃该任务.
            AbortPolicy:处理程序遭到拒绝时将抛出 RejectedExecutionException
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //等待所有任务调度完成在关闭线程池,保证所有的任务被正确处理
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //线程池关闭时等待其他任务的时间,不能无限等待,确保应用最后能被关闭。而不是无限期阻塞
        executor.setAwaitTerminationSeconds(60);
        //线程池初始化
        executor.initialize();
        return executor;
    }
}

定时任务源码修改如下:

@Component
public class ScheduledTask {
 
    @Scheduled(cron = "0/1 * * * * ?")
    //指定自定义线程池
    @Async("asyncTaskExecutor")
    public void scheduledTask1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
    }
 
    @Scheduled(cron = "0/1 * * * * ?")
    @Async//未指定线程池,则使用默认线程池
    public void scheduledTask2() {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask2 " + System.currentTimeMillis());
    }
}

控制台执行结果如下:

从图中依据线程名字,看到任务1均有自定义线程池defineAsyncTask-*执行,同时验证默认线程池SimpleAsyncTaskExecutor一直创建新线程执行。

2.定义全局线程池

上面需在@Async()注解中指定使用自定义线程池才有效,如果我们即不想指定线程池,又不想使用默认线程池池—全局线程池。

定义全局线程池可以通过实现 AsyncConfigurer 或者继承 AsyncConfigurerSupport。

源码如下:

@Configuration
public class AsyncGlobalConfig extends AsyncConfigurerSupport {
    private static final String THREAD_PREFIX = "defineGlobalAsync-";
 
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix(THREAD_PREFIX);
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}

再次执行上面的任务,结果如下:

任务1,指定自定义线程池,则有该线程池执行任务,其余未指定线程池,则使用自定义的全局线程池执行任务。

线程池指定策略:

可以很明确的知道,任务因为超过了执行线程+缓冲队列长度,而被拒绝了。

所有,默认情况下,线程池的拒绝策略是:当线程池队列满了,会丢弃这个任务,并抛出异常。

配置拒绝策略

虽然线程池有默认的拒绝策略,但实际开发过程中,有些业务场景,直接拒绝的策略往往并不适用,有时候我们可能会选择舍弃最早开始执行而未完成的任务、也可能会选择舍弃刚开始执行而未完成的任务等更贴近业务需要的策略。所以,为线程池配置其他拒绝策略或自定义拒绝策略是很常见的需求,那么这个要怎么实现呢?

下面就来具体说说今天的正题,如何为线程池配置拒绝策略、如何自定义拒绝策略。

看下面这段代码的最后一行,setRejectedExecutionHandler方法就是为线程池设置拒绝策略的方法:

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

//...其他线程池配置

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

ThreadPoolExecutor中提供了4种线程的策略可以供开发者直接使用,你只需要像下面这样设置即可:

// AbortPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

// DiscardPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

// DiscardOldestPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

// CallerRunsPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

这四个策略对应的含义分别是:

  • AbortPolicy策略:默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
  • DiscardPolicy策略:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
  • DiscardOldestPolicy策略:如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
  • CallerRunsPolicy策略:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。

而如果你要自定义一个拒绝策略,那么可以这样写:

executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 拒绝策略的逻辑
    }
});

当然如果你喜欢用Lamba表达式,也可以这样写:

executor.setRejectedExecutionHandler((r, executor1) -> {
    // 拒绝策略的逻辑
});

好了,今天的学习就到这里!

### Spring Boot中配置线程池实现多线程处理 #### 使用`@Configuration`类定义线程池 Bean 为了在Spring Boot应用程序中配置自定义线程池,可以通过创建一个新的配置类并使用`@Bean`注解来声明一个`ThreadPoolTaskExecutor`类型的bean。这允许开发者指定诸如核心线程数、最大线程数等参数。 ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @Configuration public class ThreadPoolConfig { @Bean(name = "taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 设置核心线程数为CPU核数的两倍 int coreSize = Runtime.getRuntime().availableProcessors() * 2; // 设置最大线程数为CPU核数的四倍 int maxSize = Runtime.getRuntime().availableProcessors() * 4; executor.setCorePoolSize(coreSize); executor.setMaxPoolSize(maxSize); executor.setQueueCapacity(100); // 队列容量设为100 executor.setThreadNamePrefix("MyTask-"); // 线程名称前缀 // 初始化时启动最小数量的核心线程 executor.initialize(); return executor; } } ``` 上述代码片段展示了如何通过编程方式配置线程池[^3]。 #### 创建异步方法执行任务 为了让某个服务的方法能够被多个线程并发调用,可以在该方法上加上`@Async`注解,并确保已经启用了异步支持(即,在任意配置文件里添加`@EnableAsync`)。下面的例子说明了这一点: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class AsyncTaskService { private final ThreadPoolTaskExecutor taskExecutor; @Autowired public AsyncTaskService(ThreadPoolTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @Async("taskExecutor") // 指定使用的线程池名 public void executeAsyncTask(int i){ System.out.println( Thread.currentThread().getName()+" is executing : "+i+" at time:"+System.currentTimeMillis()); } } ``` 这段程序实现了简单的后台任务调度功能,其中每个请求都会由不同的工作线程独立完成[^1]。 #### 测试异步任务的效果 最后一步是在控制器或者其他组件内测试这个新特性的工作情况。这里给出了一种可能的方式来进行验证: ```java @RestController @RequestMapping("/tasks") public class AsyncController { @Autowired private AsyncTaskService asyncTaskService; @GetMapping("/async/{count}") public String invokeTasks(@PathVariable Integer count) throws InterruptedException{ long start=System.currentTimeMillis(); for (int i = 0 ; i<count;i++){ asyncTaskService.executeAsyncTask(i); } while(System.currentTimeMillis()-start<5000){} //等待一段时间让所有任务结束 return "All tasks have been submitted!"; } } ``` 此段代码提供了一个HTTP端点用于触发一系列异步操作,并且会暂停主线程直到预计所有的子任务都已完成才返回响应给客户端[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值