线程池主要是为了避免频繁创建线程和销毁线程。当单个任务处理时间比较长或者是需要处理的任务量很大,为了避免系统效率降低,可以创建线程池,复用线程。
创建线程池
1 | //五个参数的构造函数 |
corePoolSize,核心线程数。
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程。
即使核心线程处于闲置状态,核心线程也不会被销毁。
maximumPoolSize,最大线程数。线程池中能创建的最大的线程数。
keepAliveTime.当非核心线程数闲置时长超过keepAliveTime,就会将其销毁。
workQueue。
该线程池中的任务队列:维护着等待执行的Runnable对象。
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。
可以使用无界队列或者是有界队列。
使用无界队列的话,这样队列永远不会满,也就是说线程池内线程数只能是核心线程数。
handler:饱和策略,当任务数量超过了最大线程数,应该怎么做
ThreadPoolExecutor的策略
当一个任务被添加进线程池时:
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePoolSize,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,根据设定的策略进行处理

饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。
线程池种类
newFixedThreadPool,固定线程数的线程池。
1
2
3
4
5public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool。
1
2
3
4
5public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 它是一个可以无限扩大的线程池;
- 它比较适合处理执行时间比较小的任务;
- corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
- keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
- 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
newScheduledThreadPool().
1
2
3
4
5
6
7public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
//DelayedWorkQueue 这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务newSingleThreadPool().
1
2
3
4
5
6public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
为什么不建议使用Executors的方法创建ThreadPoolExecutor对象?
例如,FixedThreadPool和SingleThreadPool允许请求的队列长度是Integer.Max_value,很容易出现排队线程过多的情况。
关闭线程池
关闭线程池,可以通过shutdown和shutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdown和shutdownNow还是有不一样的地方:
shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程
配置线程池参数
CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。
线程池状态
线程池有五种状态,running,shutdown,stop,tidying,terminated。
running:运行状态。
shutdown:不再接收新提交的任务,但是可以处理阻塞队列中的任务。调用shutdown方法会达到这种状态。
stop:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。调用shutdownnow方法会达到这个状态。
tidying:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
Future模式
- 通过它们可以在任务执行完毕之后得到任务执行结果。
- Future模式是一个异步调用。
- Future其实相当于一个占位符,当我们提交线程任务之后,可以不必等待任务完成,使用Future占位,当执行完成后,自然可以通过future.get()获取执行结果。
1 | public class FutureDemo1 { |
我们观察一下控制台的输出,发现依旧耗费 3s 来完成这次耗时操作,并没有比同步调用方式快。但是提交任务(非阻塞)和获取结果(阻塞)之间我们可以进行一些额外的操作,而这将形成一个并行执行的效果。
我们会发现如果 future 提交给线程池执行之后立刻 get(),其实执行效率并不会变高,因为执行结果没有完成其实会将线程阻塞,反而由于线程的开销会比同步调用更慢。这种将来式的 future 适用多个耗时操作并发执行的场景。
future并不是通过通知的方式来通知线程任务完成的。
execute and submit
向线程池中提交任务主要有submit以及execute两种方式。submit和execute最大的区别就是submit有返回值,而execute没有返回值。但是实际上submit的底层使用的还是execute。
submit
是将要执行的任务包装为FutureTask来提交,使用者可以通过FutureTask来拿到任务的执行状态和执行最终的结果,最终调用的都是execute方法,所以,submit提交任务有返回值。