在开始这篇文章之前,首先提出几个问题:
- 什么是线程池?
- 为什么要用线程池?
- 线程池的7个核心参数及简单使用
- 线程池的执行流程?
- 进阶:线程池是怎么区别核心线程和非核心线程的?
什么是线程池
线程池,即持有管理线程的工具,能接收用户的任务并分配给其中一个线程异步执行。
为什么要用线程池
new Thread()执行的缺点
使用传统的new Thread方法,手动地执行start 运行后,则新建了一个线程执行指定的任务,而线程不能复用,在执行完成之后就自动地销毁了。之后再执行时,又要重新创建线程,这样造成了线程的创建和销毁的性能损耗。同时,手动创建线程通常不会考虑系统负载情况,当任务某个点激增时,无限制地新建线程可能导致系统宕机。
总结: 1、线程不能复用,创建、销毁损耗性能 2、不能对线程进行管理
使用线程池的优点
线程池持有管理了线程(核心和非核心线程)。当有任务需要执行时,由线程池分配给空闲的线程进行执行,线程得到了复用,避免了创建和销毁线程带来的性能损耗。同时,线程池统一管理线程,合理地设置线程数可以提高系统利用率及防止系统宕机。因此,使用线程池解决了上述提到了手动创建线程的缺点。
总结: 1、线程能复用,提高性能 2、能对线程进行管理,保护系统
线程池的核心参数
线程池的参数总共有7个
- corePoolSize:核心线程数量,核心线程即常驻线程,创建之后不会销毁。但是设置allowCoreThreadTimeOut为true的话,也会跟非核心线程一样,在没有执行任务一段时间后被销毁。
- maximumPoolSize:最大线程数量,包括核心线程数和非核心线程数。非核心线程会在没有执行任务一段时间后被销毁。
- keepAliveTime:保活时间,在非核心线程没有执行任务时,设置的多长时间后被销毁
- unit:时间单位,配合keepAliveTime使用。如keepAliveTime设为10,unit设为秒,则表示非核心线程没有任务执行时,10s钟后销毁该线程。
- workQueue:队列,用来暂时存放待执行的任务。
- threadFactory:创建线程的工厂,定义了线程池怎么创建新线程。可以使用默认的Executors.defaultThreadFactory(),也可以自己实现。
- rejectHandler:当线程池的线程数达到了maximumPoolSize,并且workQueue存放的任务也满了之后的策略。如AbortPolicy拒绝不接收任务,DiscardOldestPolicy丢弃workQueue中最早的任务。
线程池的创建使用
官方工具类自带
官方的Executors线程池工具类默认实现了三种线程池,尽管作者在注释中推荐,但现在极其不推荐使用:一来这三种会有OOM或线程过多导致CUP使用率占满的情况,二来更推荐开发者根据自身的业务情况,创建合适配置的线程池。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点:核心线程数为0,最大线程数不限制,使用SynchronousQueue(放入一个任务后阻塞,等有线程取出任务后才能放入)。
这表示线程池不会有常驻线程,但是非核心线程数不受限制,可能导致系统线程数过多而oom
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:只有核心线程,但任务队列容量不受限制,在任务量激增时无限制地存放进队列中,可能导致OOM
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
特点:有且只有一个核心线程,但任务队列容量不受限制,在任务量激增时无限制地存放进队列中,可能导致OOM
自定义参数使用
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(1),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i=0; i<10; i++) {
final int n = i;
threadPoolExecutor.execute(()->{
System.out.println("第" + n + "个线程在执行");
});
}
}
如上,表示创建了一个核心线程数为2,最大核心线程数为10,10s没执行任务销毁非核心线程,任务队列最多能存放一个任务,默认的ThreadFactory和拒绝策略。通过execute方法传入一个Runnable对象(上面使用了lambda匿名表达式)即提交了一个任务给线程池执行。
而在使用时,我们一般根据实际场景配置参数。
线程池的执行流程
从任务角度:
当一个新的任务交给线程池执行时:首先看当前的线程数是否小于核心线程数,是的话创建一个新的线程来执行该任务(即使此时有线程是空闲的);否的话,看workingQueue是否已满,未满的话将该任务放入workingQueue中;已满的话再看当前线程数是否小于最大线程数(maximumPoolSize),是的话创建一个新的线程来执行该任务,否的话执行RejectExecutionHandler拒绝策略。
从线程的生命周期角度:
线程池初始时线程数为0(默认,可执行prestartAllCoreThreads初始化所有核心线程)。当第一个任务来时,线程池发现当前的线程数是否小于核心线程数,于是创建出一个新的核心线程来执行任务;核心线程执行完线程后,不会被销毁,会定时地从队列看有没有任务取来执行;而当某一个任务来时,当前线程数大于等于核心线程数,队列又满了,当前线程数又大于等于最大线程数,于是创建非核心线程数来执行该任务;非核心线程执行完之后,也会定时从队列看有没有任务可以执行,同时会开始计时,若keepAliveTime时间内没有任务执行,该线程会被销毁。若在此时间内线程执行了任务则会重新开始计时。
超级进阶:线程池是怎么区分核心线程和非核心线程的?
刚刚介绍线程池的执行流程时,多次提到了核心线程和非核心线程,区别是非核心线程在配置的时间内没有执行任务会被销毁,而核心线程不会。那么,线程池是怎么进行区分核心线程和非核心线程的呢?非核心线程在配置时间没有任务会被销毁,那是在每个非核心线程上有一个计时器吗?
其实线程池是没有区分核心线程和非核心线程的!
线程池只有coresize和maximumsize,在数量上进行的逻辑处理,并没有在线程个体上做区分。 之所以所有的教ba程gu都在强调核心线程和非核心线程,个人猜测是引入核心线程和非核心线程的概念,更便于理解学习。但是又在具体实现上又不加以说明。
当新建了线程,线程首先执行任务;执行完成之后会从workQueue队列中取任务。队列可能为空,因此线程取任务可能会阻塞。在从队列获取任务前,线程池会先进行判断,当线程数量>coreSize时,说明可以消减线程了,就会给该线程设置从队列取任务的最长阻塞时间(keepAliveTime),超时返回null,即表示该线程空闲了keepAliveTime时间,并且线程池数大于corePoolSize核心线程数,就会将该线程销毁。