前言
java中经常需要用到多线程来处理一些业务,但是我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。众所周知,线程有五种基本状态,分别是:
1、NEW(初始化)状态
2、RUNNABLE(可运行)状态,也称就绪状态
3、RUNNING(运行)状态
4、BLOCKED(阻塞)状态
5、DEAD(死亡)状态
所有可想而知,一个线程从创建到使用需要多少时间,又多么麻烦。所以我们需要一种更加方便和安全的,顺便将这些繁琐的过程简单化的线程创建使用方式。这个时候,线程池就登场了。
1.首先我们需要知道线程池是个啥?
2.使用线程池有什么好处?
3.线程池解决了什么问题?
4.线程池怎么使用?
何为线程池
线程池(Thread Pool)是一种基于池化思想管理线程的工具,一种线程使用模式。线程过多会带来额外的开销,其中包括创建与销毁线程的开销、调度线程的开销等等,进而影响缓存局部性和整体性能。简单点说线程池就是预先创建好多n个空闲线程,节省了每次使用线程时都要去创建的时间,使用时只要从线程池中取出,用完之后再还给线程池。就像现在的共享物品一样,需要的时候只要去“借”,用完之后只需还回去就行。“池”的概念都是为了节省时间而创建的,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
使用线程池的好处
- 降低系统资源消耗:通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度:当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控:因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
线程池解决的问题是什么
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
为了解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想,主要实现以下两点:
- 提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
- 线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。
线程池怎么使用?
线程池的总体设计
Java SE5增加了juc包来简化并发编程,而juc包中的Executor执行器来管理Thread对象。Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许我们管理异步执行的任务,而无须显示的管理线程的生命周期,是启动线程的优先选择。Executors 是Executor、ExecutorService、ScheduledExecutorService以及ThreadFactory、Callable的工厂及实用方法。
线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类,我们可以通过ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。
ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:
- 扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;
- 提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
- 直接申请线程执行该任务;
- 缓冲到队列中等待线程执行;
- 拒绝该任务;线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
线程池的工作流程
- 判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,已满则执行第二步。
- 判断任务(阻塞)队列是否已满,没满则将新提交的任务添加在工作队列,已满则执行第三步。
- 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
线程池的创建
线程池可以自动创建也可以手动创建:
- 自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool、newSingleThreadScheduledExecutor、newWorkStealingPool。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
- 手动创建主要用ThreadPoolExecutor类,体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
ThreadPoolExecutor中重要的七大核心参数
corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。
maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
unit:keepAliveTime的时间单位
workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建。
handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。
线程池的具体介绍
本文通过一个模拟网络下载功能,开启多任务下载操作,其中每条下载都开辟新线程的例子,来具体介绍线程池的常用几种方法。
我们每次点击下载按钮,都会开启一个新的线程来执行下载流程,可以看下图
也就是说,我们每个任务开辟的都是一个新的线程,假如我们下载任务量非常庞大时,那开辟的线程将不可控制,先不说性能问题,如果出现了线程安全问题或者是线程的调度,处理起来都是非常困难的。所以这种情况下,非常的有必要引入线程池来管理这些线程。
newSingleThreadPool
newSingleThreadPool,创建Single线程的线程池,只有1个核心线程,没有非核心线程,保证所有任务按照指定顺序(先进先出(FIFO,LIFO))执行,这里的时间为 0 意味着无限的生命,就不会被摧毁了。它的创建方式源码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
通过模拟网络下载功能的例子, newSingleThreadPool 测试结果如下:
由于我们的线程池中使用的从始至终都是单个线程,所以这里的线程名字都是相同的,而且下载任务都是一个一个的来,直到有空闲线程时,才会继续执行任务,否则都是等待状态。
newFixedThreadPool
newFixedThreadPool,创建一个固定大小的线程池,我们只需传入一个固定的核心线程数,便可控制并发的线程数,超出的线程会在队列中等待,并且核心线程数等于最大线程数,而且它们的线程数存活时间都是无限的,看它的创建方式:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
对比 newSingleThreadPool,我们可以传入一个自定义的核心线程数来执行任务,两者比较相似。通过newFixedThreadPool(2)给它传入了 2 个核心线程数,看看下载效果如何:
显然,它就可以做到并发的下载,我们两个下载任务可以同时进行,并且所用的线程始终都只有两个,因为它的最大线程数等于核心线程数,不会再去创建新的线程了,所以这个方式也可以,但最好还是运用下面一种线程池。
newCachedThreadPool
newCachedThreadPool,可以进行缓存的线程池,若线程数超过任务所需,那么多余的线程会被缓存一段时间后才被回收,若线程数不够,则会新建线程,意味着它的线程数是最大的,无限的。核心线程数为 0,最大线程数被设置成了Integer.MAX_VALUE
,任务等待时间60秒。通常用于执行一些生存期很短的异步型任务。这里要考虑线程的摧毁,因为不能够无限的创建新的线程,所以在一定时间内要摧毁空闲的线程。看看创建的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
虽然没有核心线程数,但是我们的最大线程数没有限制,所以一点全部开始下载,就会创建出 5 条新的线程同时执行任务,从上图的例子看出,每天线程都不一样。看不出这个线程池的效果,下面我们通过修改这个逻辑。
首先,我们点开始下载,只会下载前面三个,为了证明线程的复用效果,我这里又添加了一个按钮,在这个按钮中继续添加后面两个下载任务。
那么,当线程下载完毕时,空闲线程就会复用,结果显示如下,复用线程池的空闲线程:
另一种情况,当线程池中没有空闲线程时,这时又加了新的任务,它就会创建出新的线程来执行任务,结果如下:
但是由于这种线程池创建时初始化的都是无界的值,一个是最大线程数,一个是任务的阻塞队列,都没有设置它的界限,当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足。
newScheduledThreadPool
newScheduledThreadPool,这个表示的是有计划性的线程池,参数为核心线程,非核心线程数为Integer的最大值,空闲关闭时间为10毫秒,就是在给定的延迟之后运行,或周期性地执行。和我们前端用的setTimeout和setInterval这两类JS定时器类似。它的构造函数如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
内部有一个延时的阻塞队列来维护任务的进行,延时也就是在这里进行的。我们把创建 newScheduledThreadPool 的代码放出来,这样对比效果图的话,显得更加直观。
//参数2:延时的时长
scheduledExecutorService.schedule(th_all_1, 3000, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_2, 2000, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_3, 1000, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_4, 1500, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_5, 500, TimeUnit.MILLISECONDS);
这个线程池好像不是很常用,做个了解就好了。
newSingleThreadScheduledExecutor
newSingleThreadScheduledExecutor,创建一个单线程的可以执行延迟任务的线程池,此线程池可以看作是 ScheduledThreadPool 的单线程池版本。
newWorkStealingPool
newWorkStealingPool,这个是 JDK1.8 版本加入的一种线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面4种都不一样,用的是 ForkJoinPool 类,构造函数代码如下:
/**
* Creates a thread pool that maintains enough threads to support
* the given parallelism level, and may use multiple queues to
* reduce contention. The parallelism level corresponds to the
* maximum number of threads actively engaged in, or available to
* engage in, task processing. The actual number of threads may
* grow and shrink dynamically. A work-stealing pool makes no
* guarantees about the order in which submitted tasks are
* executed.
*
* @param parallelism the targeted parallelism level
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code parallelism <= 0}
* @since 1.8
*/
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
从上面代码的介绍,最明显的用意就是它是一个并行的线程池,参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面5种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。从介绍中,还说明这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作。
如下图,任务的执行是无序的,哪个线程抢到任务,就由它执行:
通过这个案例,我们把线程池学习了一遍,总结一下线程池在哪些地方用到,比如网络请求、下载、I/O操作等多线程场景,我们可以引入线程池,一个对性能有提升,另一个就是可以让管理线程变得更简单。
execute()和submit()方法
- execute(),执行一个任务,没有返回值。
- submit(),提交一个线程任务,有返回值。
线程池的拒绝策略
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
在此之间,我们需要知道一个前提,所有拒绝策略都实现了接口 RejectedExecutionHandler:
public interface RejectedExecutionHandler {
/**
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
这个接口只有一个 rejectedExecution 方法。r 为待执行任务;executor 为线程池;方法可能会抛出拒绝异常。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,JDK 默认提供下面四种拒绝策略,默认使用 AbortPolicy
如何配置线程池
线程数设置(根据任务特性)
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- 线程数<=机器cpu逻辑核心数+1(+1是为了利用页缺失的情况),太多会频繁上下文切换,浪费性能
- 对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的效 率。(即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的 时钟周期不会被浪费。)
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
- 监控系统,核心态越少越好
- 没参考:机器cpu逻辑核心数*2
- 最佳(需监控后才能得出):cpu核心数期望cpu利用率 (1+等待时间与计算时间比率(w/c))
混合型任务(cpu密集和io密集55开)
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。