一、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的处理流程
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、使用线程池的优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
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
(1)newCachedThreadPool
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
(2)newFixedThreadPool
创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
(3)newSingleThreadExecutor
创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
(4)newScheduledThreadPool
适用于执行延时或者周期性任务。