目录
一、SpringBoot异步线程池
1、定义线程池
2、线程池的使用
二、ThreadPoolTaskExecutor和ThreadPoolExecutor区别
1、ThreadPoolExecutor的处理流程
2、四种Reject预定义策略
三、Java线程池
1、使用线程池的优势
2、什么是阻塞队列?
3、线程池为什么要是使用阻塞队列?
4、如何配置线程池?
5、Java中提供的线程池
(1)newCachedThreadPool
(2)newFixedThreadPool
(3)newSingleThreadExecutor
(4)newScheduledThreadPool
代码示例:配置一个线程池,这里使用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
创建了一个线程池,同时设置了以下这些参数:
CallerRunsPolicy
策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务说明:setWaitForTasksToCompleteOnShutdown(true)
该方法就是这里的关键,用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。同时,这里还设置了setAwaitTerminationSeconds(60),该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
使用多线程,往往是创建Thread,或者是实现runnable接口,用到线程池的时候还需要创建Executors,spring中有十分优秀的支持,就是注解@EnableAsync就可以使用多线程,@Async加在线程任务的方法上(需要异步执行的任务),定义一个线程任务,通过spring提供的ThreadPoolTaskExecutor就可以使用线程池。
线程池的使用在Spring中非常简单,只要设置两个注解就可以了
(1)@EnableAsync // 开启异步任务
(2)@Async("taskExecutor") // 申明为异步方法,指定线程池名称
注: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效
@Slf4j
@Component
public 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)
@SpringBootTest
public 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:18
2020-04-16 15:23:18.381 INFO 1828 --- [ scheduling-1] demo.spring.tasks.ScheduledTasks : ScheduledTasks2 - The time is now 15:23:18
2020-04-16 15:23:19.475 INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task : 完成任务三,耗时:5640毫秒
其中任务一会报错,因为没有配置相关的redis配置,但是并不影响其他任务的执行。
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 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;
}
threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
RejectedExecutionHandler handler: 用来拒绝一个任务的执行,有两种情况会发生这种情况:
一是在execute方法中若addIfUnderMaximumPoolSize(command)为false,即线程池已经饱和;
二是在execute方法中, 发现runState!=RUNNING || poolSize == 0,即已经shutdown,就调用ensureQueuedTaskHandled(Runnable command),在该方法中有可能调用reject。
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 异常。
(1)ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。
(2)ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃。
(3)ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃。
(4)ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。
阻塞队列BlockingQueue,相当我们经常接触的List,但如果BlockQueue是空的,这时如果有线程要从这个BlockingQueue取元素的时候将会被阻塞进入等待状态,直到别的线程在BlockingQueue中添加进了元素,被阻塞的线程才会被唤醒。同样,如果BlockingQueue是满的,试图往队列中存放元素的线程也会被阻塞进入等待状态,直到BlockingQueue里的元素被别的线程拿走才会被唤醒继续操作。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得线程不至于一直占用cpu资源。
线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下while (task != null || (task = getTask()) != null) {...}。
不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?
(1)CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
(2)IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
(3)混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
Executors类提供了4种不同的线程池:newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
适用于执行延时或者周期性任务。