线程池为线程的生命周期开销问题和资源不足问题提供了解决方案,通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。合理的利用线程池能带来很大的好处:
- 降低资源消耗。过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
一、线程池的使用
1、线程池的创建
通过 ThreadPoolExecutor 类来创建一个线程池,所以先从他的构造函数入手:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
以下是几个参数的说明:
-
corePoolSize:线程池的基本大小。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
-
maximumPoolSize:线程池允许创建的最大线程数。如果线程队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务,但是如果使用了无界的任务队列这个参数就没有什么效果。
-
keepAliveTime:线程池的工作线程空闲后保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
-
TimeUnit:线程活动保持时间的单位。
-
BlockingQueue:用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
(1)ArrayBlockingQueue:基于数组结构的队列,按 FIFO 原则对元素进行排序。
(2)LinkedBlockingQueue:基于链表结构的阻塞队列,按 FIFO 原则对元素进行排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。
(3)SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
(4)PriorityBlockingQueue:一个具有优先级得无限阻塞队列。 -
ThreadFactory:创建线程的工厂。
-
RejectedExecutionHandler:饱和策略。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
2、提交任务
我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。execute 方法的输入是一个 Runnable 类的实例。
executor.execute(new Runnable() {
@Override
public void run() {
}
});
我们也可以使用 submit 方法来提交任务,返回值是一个 Future<?>对象,我们可以通过这个对象来判断任务是否执行成功,通过 get 方法来获取返回值,get 方法会阻塞直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
3、线程池的关闭
线程池的关闭可以通过调用 shutdown() 方法或shutdownNow() 方法来执行。
- shutdown():将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
二、线程池分析
线程池的主要工作流程如下图:

从流程图上可以看出,当提交一个任务到线程池时,线程池的处理流程如下:
- 首先判断基本线程池是否已满?没满,创建一个工作线程来执行任务;满了,则进入下一个流程。
- 其次判断工作队列是否已满?没满,则将新提交的任务存储在工作队列中;满了,则进入下个流程。
- 最后判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务;满了,则交给饱和策略来处理这个任务。
三、Executors 分析
阿里发布的最新的 Java 开发手册中强制线程池的使用不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这种处理方式让实现的程序员更加明确线程池的运行规则,避免资源耗尽的风险。
虽然 Executors 的方式不建议使用,但是作为面试中的常客,还是要强行分析一波 Executors 的那几个工厂方法:
1、newCachedThreadPool
创建一个可缓存线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小,但是一定要注意控制任务的数量,否则由于大量线程的同时运行,容易造成系统瘫痪。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
该方式适用于执行很多短期异步的小程序或者负载较轻的服务器。
2、newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
该方式适用于执行长期的任务,性能好很多。
3、newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
该方式适用于一个任务一个任务执行的场景。
4、NewScheduledThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
四、线程池调优
对于线程池的大小不能过大,也不能过小。过大会有大量的线程咋相对较少的CPU和内存上竞争,过小又会导致空闲的处理器无法工作,浪费资源,降低吞吐率。所以如何合理的给出线程池的大小非常的重要。
对于线程池的大小设定,一般要考虑CPU个数,内存大小,任务类型等问题。有种简单的估算方式,设N为CPU个数:
- 对于CPU密集型的应用,线程池的大小设置为N+1。
- 对于I/O密集型的应用,线程池的大小设置为2N+1 。
这种设置方式适合于一台机器上的应用的类型是单一的,并且只有一个线程池,实际情况还需要根据实际的应用进行验证。
在I/O优化中,以下的估算公式可能更合理
最佳线程数量 = ((线程等待时间+线程CPU时间)/ 线程CPU时间)* CPU个数
由公式可得,线程等待时间所占比例越高,需要越多的线程。
线程CPU时间所占比例越高,所需的线程数越少。
参考:如何合理确定线程池大小

2万+

被折叠的 条评论
为什么被折叠?



