线程池梳理
Posted frankltf
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程池梳理相关的知识,希望对你有一定的参考价值。
线程池
目的
- 降低资源消耗
- 通过重复利用已创建的线程降低线程的创建和销毁的消耗
- 提高利用率
- 当任务到达时,可以省去线程的创建时间,直接去执行。
- 方便管理
- 线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配,调优和监控。
线程池的四种创建方式
- newCachedThreadPool
- 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public class TestTwo {
public static void main(String[] args){
ExecutorService executorService= Executors.newCachedThreadPool();
for (int i=0; i<20;i++){
final int temp=i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+","+temp);
}
});
}
}
}
- newFixedThreadPool
- 可控制线程最大并发数,超出的线程会在队列中等待。
public class TestTwo {
public static void main(String[] args){
ExecutorService executorService= Executors.newFixedThreadPool(3);
for (int i=0; i<20;i++){
final int temp=i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+","+temp);
}
});
}
}
}
- newSingleThreadExecutor
- 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- newScheduledThreadPool
- 创建一个定长线程池,支持定时及周期性任务执行。
线程池的工作原理
当一个任务提交至线程池之后,
- 线程池首先判断核心线程池里的线程是否已经满了。如果不是,则创建一个新的工作线程来执行任务。否则进入2.
- 判断工作队列是否已经满了,倘若还没有满,将线程放入工作队列。否则进入3.
- 判断线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行。如果线程池满了,则交给饱和策略来处理任务。
ThreadPoolExecutor
当一个任务提交至线程池之后,
- 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.
- 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.
- 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。
ScheduleThreadPoolExecutor
- ScheduleThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后执行任务,或者定期执行任务。功能与Timer类似,但比Timer更强大,Timer对应的是单个后台线程,而ScheduleThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
-
线程池饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:
1、AbortPolicy:直接抛出异常
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。 -
关闭线程池
- 原理
- 遍历线程池中的所有线程,然后逐个调用线程的interrupt方法来中断线程.
- 关闭方式
- shutdown
将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程. - shutdownNow
将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程.
- shutdown
线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
线程池有5种状态
- 线程池的5种状态是:Running, SHUTDOWN, STOP, TIDYING, TERMINATED。
- RUNNING
(01) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态!
道理很简单,在ctl的初始化代码中(如下),就将它初始化为RUNNING状态,并且"任务数量"初始化为0。
- 2.SHUTDOWN
(01) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(02) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
- 3.STOP
(01) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(02) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
- 4.TIDYING
(01) 状态说明:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(02) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
- 5.TERMINATED
(01) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(02) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
配置线程池需要考虑哪些因素
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:
- CPU密集型:尽可能少的线程,Ncpu+1
- IO密集型:尽可能多的线程, Ncpu*2,比如数据库连接池
混合型:CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。
- CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数
- IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1
- 而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。
- 若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。
- 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
- 比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) CPU数目 - 可以得出一个结论: 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。
- 比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)8=32。这个公式进一步转化为:
参考
- 《Java并发编程艺术》
以上是关于线程池梳理的主要内容,如果未能解决你的问题,请参考以下文章
java面试总躲不过的并发: 线程池ThreadPoolExecutor基础梳理