线程池现在是面试比较基础的知识点了,很多人可能都会去学一下线程池的几个参数,但是对真实使用过程中的问题却思考的不多。这里我们来仔细分析一下线程池的一些问题。
为什么要用线程池
我们当然可以手动创建,像下面这样:
public void threadCreateTest() {
for (int i = 0; i < 1000; i++) {
final int x = i;
new Thread(() -> {
System.out.println("thread-" + x);
}).start();
}
}
也可以用线程池,
public void threadPoolTest() {
ThreadPoolExecutor threadPoolExecutor = new
ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 1000; i++) {
final int x = i;
threadPoolExecutor.submit(() -> System.out.println("thread-" + x));
}
}
但是,对于上面两个功能大致相同的方法,其中一个要创建1000个线程,另外一个只需要创建5个即可。同一个电脑上统计这两个的执行时间第一个time: 166ms,第二个time: 91ms,可见,并不是说创建的线程越多,执行速度就越快(线程的创建过程也会消耗系统资源)。
怎么用
创建线程池
可以使用Executors
来创建线程池,些方法创建出来的线程池主要有四种:
-
newFixedThreadPool
固定线程数量的线程池,
BlockingQueue
使用的是LinkedBlockingQueue
(无长度限制) -
newSingleThreadExecutor
创建一个只有一个
worker
线程的线程池,BlockingQueue
使用的是LinkedBlockingQueue
-
newCachedThreadPool
创建一个按需创建新线程的线程池,会复用之前创建的线程池。此线程池的BlockingQueue使用的是非公平的
SynchronousQueue
-
newScheduledThreadPool
使用的
BlockingQueue
是DelayedWorkQueue
-
newWorkStealingPool
1.8新增加的,不是ThreadPoolExecutor,而是ForkJoinPool,会创建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。
上面是使用JDK提供的线程池工厂来创建线程池,也可以自己使用ThreadPoolExecutor的构造函数去创建线程池。
线程池参数
其实ThreadPoolExecutor里面的注释已经讲的很清楚了,构造函数一共有7个
-
corePoolSize(核心线程数),在一般情况下,当执行任务的线程数量达到核心线程数之后,即便这里面的某些线程是空闲的,也仍会存在,除非设置了
allowCoreThreadTimeOut
参数 -
maximumPoolSize(最大线程数),当前正在执行任务的线程数量等于核心线程数,且队列已满的情况下才会继续创建新的线程来执行任务,直到线程数量等于最大线程数。所以如果使用无界阻塞队列(LinkedBlockingQueue),则maximumPoolSize不会起作用,而如果最大线程数=核心线程数,这就是一个固定线程数的线程池。
-
keepAliveTime(空闲线程存活时间),当前执行任务的线程数大于核心线程数,且部分线程处于空闲状态,则会被回收,此参数结合
TimeUnit
即为空闲多久之后会被回收 -
unit(TimeUnit 时间单位,会转换为纳秒),结合
keepAliveTime
使用,用来明确空闲的时间。 -
workQueue(任务队列)放置需要执行的任务的队列,必需是
BlockingQueue(阻塞队列)
-
threadFactory(线程工厂)创建线程的工厂,可以指定线程分组、名称等,如果使用默认的线程工厂,此线程池里面的线程都属于一个group。
-
handler(任务拒绝策略
RejectedExecutionHandler
),即新submit的任务没有资源执行,线程数量已经达到最大线程数,且任务队列已满,提供了四种拒绝策略,默认使用的是AbortPolicy
。-
AbortPolicy
拒绝提交的任务,抛出RejectedExecutionException异常。
-
CallerRunsPolicy
当前提交任务的线程执行任务。
-
DiscardOldestPolicy
取消当前未执行且等待(排队)时间最久的任务(在BlockingQueue中),再交给线程池执行。
-
DiscardPolicy
只是取消要提交的任务,不抛出异常。
-
其他方法
多数时候,我们使用线程池只是直接submit,或者有些场景需要等待任务很行完,拿到任务执行的结果,加上CountDownLatch等。
而ThreadPoolExecutor还给我们提供了其他的方法(有很多,get类型的我们略过),只看部分方法。
-
beforeExecute 执行任务前,也是模板方法的一种使用,可以用来重新初始化(re-intialize)ThreadLocals,或者记录日志等
-
afterExecute 执行任务后
-
finalize 执行shutdown
-
isShutdown 判断是不是shutdown isTerminated 所有任务在shutdown后执行完成,返回true,如果没有先执行过shutdown或者shutdownNow,则肯定不为true isTerminating 即正在终止中,当调用了shutdown或者shutdownNow后,并没有完成terminated,返回true。
-
prestartAllCoreThreads 启动所有核心线程,使它们空闲等待工作,会覆盖仅在执行新任务时启动核心线程的默认策略,返回启动的线程数
-
prestartCoreThread 启动一个核心线程,使其空闲等待工作,会覆盖执行新任务时启动核心线程的默认策略,如果所有核心线程都已启动,此方法将返回 false
-
purge 尝试从工作队列中删除所有已取消的 Future 任务
-
remove 从任务队列中移除任务
-
shutdown 有序关闭,执行先前提交的任务,但不会接受新任务
-
**shutdownNow ** 尝试停止所有正在执行的任务,从queue中把正在等待执行的任务移除
-
terminated 一个用来扩展的方法,停止线程池,默认空实现
线程池参数设置
创建线程池时,我们需要思考如何设置核心线程数与最大线程数
一般来说,需要考虑我们运行的任务是CPU密集型和IO密集型
IO密集型可以多设置一些,如2N(N为CPU核数)或者更多,比如Tomcat设置的默认线程数是100
CPU密集型设置的是N+1(N为CPU核数)。
但是如果任务即有IO又有CPU的情况该如何设置,其实我们需要考虑的是让程序能在有IO的情况下,还能充分利用CPU,如果IO比较多(即多数时候线程等待时间长),那么就需要更多的线程,反之,则需要较少的线程。
通过查阅资料,发现多数情况下可以参考利特尔法则
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
且需要根据实际的测试情况调用。
参考资料: