Java线程池
最近生产环境的线程池出现了队列中的线程阻塞过多导致服务器不可用的情况,所以借此机会,仔细研究了一波线程池。在学习了大神的文章后,做了如下的总结和整理。
使用线程池的优势
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的底层工作原理
从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:
首先线程池判断核心线程池是否已满?没满,则创建一个工作线程来执行任务。满了,则进入下个流程。
其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor 执行 execute 方法分下面四种情况。
1)当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程大于等于corePoolSize,则将任务加入 BlockingQueue。
3)如果无法将任务加入 BlockingQueue 中(说明队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4)如果创建新线程将使当前运行的线程超出 maximumPoolSize ,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution() 方法。
线程池的几个重要参数介绍
ThreadPoolExecutor的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数介绍如下:
- corePoolSize:线程池中的常驻核心线程数。如果调用了线程池的prestartAllCoreThreads 方法,线程池会提前创建并启动所有基本线程。
- maximumPoolSize:线程池允许创建的最大线程数,此值必须大于等于1。
- workQueue:等待队列,用于存放被提交但尚未执行的任务。
- keepAliveTime:多余的空闲线程的存活时间。当前线程池的数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止。
- unit :keepAliveTime 的单位。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
- threadFactory:用于设置创建线程的工厂,可通过该工厂给线程重命名等等。一般使用默认的即可。
- handler:拒绝策略。当队列满了,并且工作线程数量大于等于线程池的最大线程数(maximumPoolSize)时,如何拒绝请求执行的 runnable 的策略。
线程池的几种等待队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
线程池的几种拒绝策略
-
AbortPolicy:直接抛出 RejectedExecutionException 异常,这是默认策略。
-
CallerRunsPolicy:用调用者所在的线程来执行任务。
-
DiscardOldestPolicy:丢弃阻塞队列中最靠前(等待最久)的任务,并执行当前任务(ThreadPoolExecutor.execute(runnable))。
-
DiscardPolicy:直接丢弃任务。
线程池怎么使用
1.使用 Executors 工具类来产生一个线程池。代码如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。如果工作线程空闲 60 秒没有被使用,会自动关闭。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
2.通过 ThreadPoolExecutor 创建一个线程池。代码如下:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
通过以上两种方式都可以得到一个线程池,那么我们日常开发中应该使用哪种呢?我们建议使用第 2 种方式来创建线程池。
Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。
《阿里巴巴Java开发手册》原文如下:
线程池的关闭
我们可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,但是它们的实现原理不同。
shutdown : 线程池拒接收新提交的任务,同时等待线程池⾥的任务执行完毕后关闭线程池。 原理是只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。
shutdownNow :线程池拒接收新提交的任务,同时⽴刻关闭线程池,线程池里的任务不再执行。原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 会首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了这两个关闭方法的其中一个,isShutdown 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow。
如何合理配置线程池的线程数
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
- 任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU 密集型任务配置尽可能少的线程数量,如配置 Ncpu+1 个线程的线程池。
IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如 2*Ncpu。
混合型的任务,如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。
线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用。
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于 taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize: 线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
- getActiveCount:获取活动的线程数。
最后,你还可以通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。