一、SpringBoot异步线程池

1、定义线程池

代码示例:配置一个线程池,这里使用spring封装的线程池

@EnableAsync // 开启异步任务@Configuration
public class TaskPoolConfig {
    @Bean("taskExecutor") // 线程池名称
    public Executor taskExecutor() {       
        // 使用Spring封装的异步线程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);               // 初始化线程数
        executor.setMaxPoolSize(20);                // 最大线程数
        executor.setQueueCapacity(200);             // 缓冲队列       
        executor.setKeepAliveSeconds(60);           // 允许空闲时间/秒       
        executor.setThreadNamePrefix("taskExecutor-");// 线程池名前缀-方便日志查找       
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());        
        executor.setWaitForTasksToCompleteOnShutdown(true);       
        executor.setAwaitTerminationSeconds(60);       
        executor.initialize();   // 初始化        
        return executor;    }
}

上面我们通过使用ThreadPoolTaskExecutor创建了一个线程池,同时设置了以下这些参数:

  • 核心线程数10:线程池创建时候初始化的线程数
  • 最大线程数20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
  • 缓冲队列200:用来缓冲执行任务的队列
  • 允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
  • 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
  • 线程池对拒绝任务的处理策略:这里采用了
  • CallerRunsPolicy
  • 策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务

说明:setWaitForTasksToCompleteOnShutdown(true)该方法就是这里的关键,用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。同时,这里还设置了setAwaitTerminationSeconds(60),该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。

2、线程池的使用

使用多线程,往往是创建Thread,或者是实现runnable接口,用到线程池的时候还需要创建Executors,spring中有十分优秀的支持,就是注解@EnableAsync就可以使用多线程,@Async加在线程任务的方法上(需要异步执行的任务),定义一个线程任务,通过spring提供的ThreadPoolTaskExecutor就可以使用线程池。

线程池的使用在Spring中非常简单,只要设置两个注解就可以了

(1)@EnableAsync                   // 开启异步任务

(2)@Async("taskExecutor")   // 申明为异步方法,指定线程池名称

注: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效

@Slf4j@Componentpublic class Task {  
  public static Random random = new Random();    
   @Autowired    private StringRedisTemplate stringRedisTemplate;   
     @Async("taskExecutor")    public void doTaskOne() throws Exception {        log.info("开始做任务一");        long start = System.currentTimeMillis();        Thread.sleep(random.nextInt(10000));        long end = System.currentTimeMillis();        log.info(stringRedisTemplate.randomKey());        log.info("完成任务一,耗时:" + (end - start) + "毫秒");    }     @Async("taskExecutor")    public void doTaskTwo() throws Exception {        log.info("开始做任务二");        long start = System.currentTimeMillis();        Thread.sleep(random.nextInt(10000));        long end = System.currentTimeMillis();        log.info("完成任务二,耗时:" + (end - start) + "毫秒");    }     @Async("taskExecutor")    public void doTaskThree() throws Exception {        log.info("开始做任务三");        long start = System.currentTimeMillis();        Thread.sleep(random.nextInt(10000));        long end = System.currentTimeMillis();        log.info("完成任务三,耗时:" + (end - start) + "毫秒");    } }

简单测试下:

@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class TaskTest {    @Autowired    private Task task;    @Test    public void test() throws Exception {        task.doTaskOne();        task.doTaskTwo();        task.doTaskThree();        Thread.currentThread().join();    }}

测试结果如下:

2020-04-16 15:23:13.834  INFO 1828 --- [ taskExecutor-1] demo.spring.tasks.Task                   : 开始做任务一2020-04-16 15:23:13.834  INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task                   : 开始做任务二2020-04-16 15:23:13.835  INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task                   : 开始做任务三2020-04-16 15:23:17.539  INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task                   : 完成任务二,耗时:3704毫秒2020-04-16 15:23:18.380  INFO 1828 --- [   scheduling-1] demo.spring.tasks.ScheduledTasks         : ScheduledTasks1 - The time is now 15:23:182020-04-16 15:23:18.381  INFO 1828 --- [   scheduling-1] demo.spring.tasks.ScheduledTasks         : ScheduledTasks2 - The time is now 15:23:182020-04-16 15:23:19.475  INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task                   : 完成任务三,耗时:5640毫秒

其中任务一会报错,因为没有配置相关的redis配置,但是并不影响其他任务的执行。

二、ThreadPoolTaskExecutor和ThreadPoolExecutor区别

ThreadPoolTaskExecutor是Spring对ThreadPoolExecutor进行封装,它实现方式完全是使用threadPoolExecutor进行实现,来看一下源码

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements SchedulingTaskExecutor {    private final Object poolSizeMonitor = new Object();    private int corePoolSize = 1;    private int maxPoolSize = 2147483647;    private int keepAliveSeconds = 60;    private boolean allowCoreThreadTimeOut = false;    private int queueCapacity = 2147483647;    private ThreadPoolExecutor threadPoolExecutor;   //这里就用到了ThreadPoolExecutor

了解了ThreadPoolTaskExecutor的相关情况,接下来看一下ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler) {        if (corePoolSize < 0 ||            maximumPoolSize <= 0 ||            maximumPoolSize < corePoolSize ||            keepAliveTime < 0)            throw new IllegalArgumentException();        if (workQueue == null || threadFactory == null || handler == null)            throw new NullPointerException();        this.corePoolSize = corePoolSize;        this.maximumPoolSize = maximumPoolSize;        this.workQueue = workQueue;        this.keepAliveTime = unit.toNanos(keepAliveTime);        this.threadFactory = threadFactory;        this.handler = handler;    }
  • int corePoolSize:线程池维护线程的最小数量.    
  • int maximumPoolSize:线程池维护线程的最大数量.    
  • long keepAliveTime:空闲线程的存活时间.    
  • TimeUnit unit: 时间单位,现有纳秒,微秒,毫秒,秒枚举值.    
  • BlockingQueue workQueue:持有等待执行的任务队列. 
  • threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)  
  • RejectedExecutionHandler handler:  线程饱和策略,用来拒绝一个任务的执行.   

RejectedExecutionHandler handler:  用来拒绝一个任务的执行,有两种情况会发生这种情况:

一是在execute方法中若addIfUnderMaximumPoolSize(command)为false,即线程池已经饱和;    

二是在execute方法中, 发现runState!=RUNNING || poolSize == 0,即已经shutdown,就调用ensureQueuedTaskHandled(Runnable command),在该方法中有可能调用reject。

1、ThreadPoolExecutor的处理流程 

springboot 子线程header传递 springboot线程池_线程池

1)当池子大小小于corePoolSize就新建线程,并处理请求

2)当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去从workQueue中取任务并处理

3)当workQueue放不下新入的任务时,新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize就用RejectedExecutionHandler来做拒绝处理

4)另外,当池子的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果无请求可处理就自行销毁

总结下:

ThreadPoolExecutor会优先创建  CorePoolSiz 线程, 当继续增加线程时,先放入Queue中,当 CorePoolSiz  和 Queue 都满的时候,就增加创建新线程,当线程达到MaxPoolSize的时候,就会抛出错误 org.springframework.core.task.TaskRejectedException

另外MaxPoolSize的设定如果比系统支持的线程数还要大时,会抛出java.lang.OutOfMemoryError: unable to create new native thread 异常。

2、四种Reject预定义策略

(1)ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。 

(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃。

(3)ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃。

(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

三、Java线程池

1、使用线程池的优势

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
  4. 提供更强大的功能,延时定时线程池。

2、什么是阻塞队列?

阻塞队列BlockingQueue,相当我们经常接触的List,但如果BlockQueue是空的,这时如果有线程要从这个BlockingQueue取元素的时候将会被阻塞进入等待状态,直到别的线程在BlockingQueue中添加进了元素,被阻塞的线程才会被唤醒。同样,如果BlockingQueue是满的,试图往队列中存放元素的线程也会被阻塞进入等待状态,直到BlockingQueue里的元素被别的线程拿走才会被唤醒继续操作。

3、线程池为什么要是使用阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得线程不至于一直占用cpu资源。

线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下while (task != null || (task = getTask()) != null) {...}。

不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

4、如何配置线程池?

(1)CPU密集型任务尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

(2)IO密集型任务可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

(3)混合型任务可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

5、Java中提供的线程池

Executors类提供了4种不同的线程池:newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

springboot 子线程header传递 springboot线程池_java_02

(1)newCachedThreadPool

用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)

(2)newFixedThreadPool

创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)

(3)newSingleThreadExecutor

创建一个单线程的线程池,适用于需要保证顺序执行各个任务。

(4)newScheduledThreadPool

适用于执行延时或者周期性任务。