Java线程池详解
# 前言
说起Java线程池,大家应该都不陌生,虽然我们常常跟线程池打交道,但是很少有人能够真正深入掌握线程池。
特别是对于从事平台研发的朋友,深入掌握线程池是非常有必要的。
本文从Java线程池的基本原理展开,到线程池上下文的传递、动态线程池治理、线程池配置的最佳实践逐步展开,再到JVM、操作系统深入理解线程池的运行原理。
# 一、线程池基础
阿里巴巴《Java 开发手册 崇山版》2020/08/03版本
第一部分编码规约,第(七)栏并发处理(P17),中有如下两条规定: 3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
# 1.1 线程池的概念
简单理解,线程池就是一个管理线程的池子。它是一个可以容纳多个线程的容器,其中的线程可以反复使用。
# 1.2 线程池的作用
它的主要特点为:线程复用;控制最大并发数;管理线程。
- 复用线程、控制最大并发数。
- 实现定时、周期等与时间相关的功能。
- 实现任务队列缓存策略和拒绝机制。
- 隔离线程环境。如:文件上传服务和数据查询服务在同一台服务器上,由于文件上传服务耗时严重,如果文件上传和数据查询服务使用同一个线程池,那么文件上传服务会影响到数据查询服务。可以通过配置独立线程池来实现文件上传和数据查询服务隔离,避免两者相互影响。
《Java 并发编程的艺术》:
在开发过程中,合理地使用线程池能够带来3个好处: 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。 但是,要做到合理利用线程池,必须对其实现原理了如指掌。
线程池的优点
线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
1、线程池的优势 (1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗; (2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行; (3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。 (4)提供更强大的功能,延时定时线程池。
# 1.3 非线程池缺点
1、因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。 2、创建线程池的消耗较高。
# 1.4 使用线程池的风险
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
# 1.4.1 死锁
任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。
虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能,在那种情况下,所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。
# 1.4.2 资源不足
线程池的一个优点在于:相对于其它替代调度机制(有些我们已经讨论过)而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程消耗包括内存和其它系统资源在内的大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。
如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。
# 1.4.3 并发错误
线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心。而最好使用现有的、已经知道能工作的实现,例如 util.concurrent 包。
# 1.4.4 线程泄漏
各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。
# 1.4.5 请求过载
仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求。
# 1.5 有效使用线程池的准则
只要您遵循几条简单的准则,线程池可以成为构建服务器应用程序的极其有效的方法:
不要对那些同步等待其它任务结果的任务排队。这可能会导致上面所描述的那种形式的死锁,在那种死锁中,所有线程都被一些任务所占用,这些任务依次等待排队任务的结果,而这些任务又无法执行,因为所有的线程都很忙。
在为时间可能很长的操作使用合用的线程时要小心。如果程序必须等待诸如 I/O 完成这样的某个资源,那么请指定最长的等待时间,以及随后是失效还是将任务重新排队以便稍后执行。这样做保证了:通过将某个线程释放给某个可能成功完成的任务,从而将最终取得某些进展。
理解任务。要有效地调整线程池大小,您需要理解正在排队的任务以及它们正在做什么。它们是 CPU 限制的(CPU-bound)吗?它们是 I/O 限制的(I/O-bound)吗?您的答案将影响您如何调整应用程序。如果您有不同的任务类,这些类有着截然不同的特征,那么为不同任务类设置多个工作队列可能会有意义,这样可以相应地调整每个池。
# 1.6 java线程池
java.util.concurrent包中我们能找到线程池的定义,其中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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。 maximumPoolSize:最大线程池大小。 keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。 unit:销毁时间单位。 workQueue:存储等待执行线程的工作队列。 threadFactory:创建线程的工厂,一般用默认即可。 handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。
corePoolSize:线程池的核心线程数目,当一个请求进来时如果当前线程池中线程数量小于这个值,则直接通过ThreadFactory新建一个线程来处理这个请求,如果已有线程数量大于等于这个值则将请求放入阻塞队列中。 maximumPoolSize:线程池的最大线程数目,当线程池数量已经等于corePoolSize并且阻塞队列也已经满了,则看线程数量是否小于maximumPoolSize:如果小于则创建一个线程来处理请求,否则使用“饱和策略”来拒绝这个请求。对于大于corePoolSize部分的线程,称作这部分线程为“idle threads”,这部分线程会有一个最大空闲时间,如果超过这个空闲时间还没有任务进来则将这些空闲线程回收。 keepAliveTime和unit:这两个参数主要用来控制idle threads的最大空闲时间,超过这个空闲时间空闲线程将被回收。这里有一点需要注意,ThreadPoolExecutor中有一个属性:private volatile boolean allowCoreThreadTimeOut;,这个用来指定是否允许核心线程空闲超时回收,默认为false,即不允许核心线程超时回收,核心线程将一直等待新任务。如果设置这个参数为true,核心线程空闲超时后也可以被回收。 workQueue:阻塞队列,超过corePoolSize部分的请求放入这个阻塞队列中等待执行。阻塞队列分为有界阻塞队列和无界阻塞队列。在创建阻塞队列时如果我们指定了这个队列的“capacity”则这个队列就是有界的,否则是无界的。这里有一点需要注意:使用线程池之前请明确是否真的需要无界阻塞队列,如果阻塞队列是无界的,会导致大量的请求堆积,进而造成内存溢出系统崩溃。 threadFactory:是一个线程池工厂,主要用来为线程池创建线程,我们可以定制一个ThreadFactory来达到统一命名我们线程池中的线程的目的。 handler:饱和策略,用来拒绝多余的请求。饱和策略有:CallerRunsPolicy:请求脱离线程池运行(调用者caller线程来运行这个任务);AbortPolicy:抛出RejectedExecutionException异常;DiscardPolicy:丢弃这个任务,即什么也不做;DiscardOldestPolicy:将阻塞队列中等待时间最久的任务删除(即队列头部的任务),将新的任务加入队尾。
corePoolSize:核心线程数。如果等于0,则任务执行完后,没有任务请求进入时销毁线程池中的线程。如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。设置过大会浪费系统资源,设置过小导致线程频繁创建。 maximumPoolSize:最大线程数。必须大于等于1,且大于等于corePoolSize。如果与corePoolSize相等,则线程池大小固定。如果大于corePoolSize,则最多创建maximumPoolSize个线程执行任务,其他任务加入到workQueue缓存队列中,当workQueue为空且执行任务数小于maximumPoolSize时,线程空闲时间超过keepAliveTime会被回收。 keepAliveTime:线程空闲时间。线程池中线程空闲时间达到keepAliveTime值时,线程会被销毁,只到剩下corePoolSize个线程为止。默认情况下,线程池的最大线程数大于corePoolSize时,keepAliveTime才会起作用。如果allowCoreThreadTimeOut被设置为true,即使线程池的最大线程数等于corePoolSize,keepAliveTime也会起作用(回收超时的核心线程)。 unit:TimeUnit表示时间单位。 workQueue:缓存队列。当请求线程数大于maximumPoolSize时,线程进入BlockingQueue阻塞队列。 threadFactory:线程工厂。用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。 handler:执行拒绝策略对象。当达到任务缓存上限时(即超过workQueue参数能存储的任务数),执行拒接策略,可以看作简单的限流保护。
# 1.7 如何关闭线程池
- es.shutdown(); 不再接受新的任务,之前提交的任务等执行结束再关闭线程池。
- es.shutdownNow(); 不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。
# 1.8 阻塞队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
队列操作:
add 增加一个元索; 如果队列已满,则抛出一个异常 remove 移除并返回队列头部的元素; 如果队列为空,则抛出一个异常 offer 添加一个元素并返回true; 如果队列已满,则返回false poll 移除并返回队列头部的元素; 如果队列为空,则返回null put 添加一个元素; 如果队列满,则阻塞 take 移除并返回队列头部的元素; 如果队列为空,则阻塞 element 返回队列头部的元素; 如果队列为空,则抛出一个异常 peek 返回队列头部的元素; 如果队列为空,则返回null
线程池都有哪几种工作队列?
主要存放等待执行的线程, ThreadPoolExecutor中支持自定义该队列来实现不同的排队队列.
ArrayBlockingQueue:先进先出队列,创建时指定大小, 有界; LinkedBlockingQueue:使用链表实现的先进先出队列,默认大小为Integer.MAX_VALUE; SynchronousQueue:不保存提交的任务, 数据也不会缓存到队列中, 用于生产者和消费者互等对方, 一起离开. PriorityBlockingQueue: 支持优先级的队列
ArrayBlockingQueue LinkedBlockingQueue DelayQueue PriorityBlockingQueue SynchronousQueue ArrayBlockingQueue ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
LinkedBlockingQueue LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
DelayQueue DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
PriorityBlockingQueue PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
SynchronousQueue SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
针对面试题:线程池都有哪几种工作队列? 我觉得,回答以上几种ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue等,说出它们的特点,并结合使用到对应队列的常用线程池(如newFixedThreadPool线程池使用LinkedBlockingQueue),进行展开阐述, 就可以啦。
# 1.9 线程池大小配置
一般根据任务类型进行区分, 假设CPU为N核
CPU密集型任务需要减少线程数量, 降低线程之间切换造成的开销, 可配置线程池大小为N + 1. IO密集型任务则可以加大线程数量, 可配置线程池大小为 N * 2. 混合型任务则可以拆分为CPU密集型与IO密集型, 独立配置. 自定义阻塞队列BlockingQueue
# 1.9.1 拒绝策略/饱和策略
饱和策略,用来拒绝多余的请求。饱 和策略有: CallerRunsPolicy:请求脱离线程池运行(调用者caller线程来运行这个任务); AbortPolicy:抛出RejectedExecutionException异常; DiscardPolicy:丢弃这个任务,即什么也不做; DiscardOldestPolicy:将阻塞队列中等待时间最久的任务删除(即队列头部的任务),将新的任务加入队尾。
- AbortPolicy 简单粗暴,直接抛出RejectedExecutionException拒绝异常,这也是默认的拒绝策略。
- CallerRunsPolicy 请求脱离线程池运行(在调用者线程中直接执行新任务)
- DiscardPolicy 丢弃这个任务,即什么也不做。
- DiscardOldestPolicy 将阻塞队列中等待时间最久的任务删除(即队列头部的任务),将新的任务加入队尾。
# 1.10 线程池异常处理
- 在runnable中 try-catch捕获异常
- 通过submit执行,Future.get接收异常
- 重写ThreadPoolExcutor.afterExcute方法,处理传递的异常引用
- 实例化时,传入自己的ThreadFactory,设置Thread.UncaughtExceptionHandler处理未检测的异常
# 1.11 合理的配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。 任务的优先级:高,中和低。 任务的执行时间:长,中和短。 任务的依赖性:是否依赖其他系统资源,如数据库连接。 任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。 建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。
# 1.12 ForkJoinPool
Doug Lea在JDK7中引入了Fork/Join框架,ForkJoinPool不同于ThreadPoolExecutor,它是一种基于"分治"思想的计算框架。java8的stream API中很多地方都有用到ForkJoinPool。ForkJoinPool中的工作线程会对自己的任务按照一定的粒度进行拆分,一个大任务拆分成多个子任务之后,子任务放入工作队列中等待执行。当一个线程的工作队列为空是可以从其他线程的工作队列中steal任务执行。这也是"work-stealing"的由来。
基本思想 ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是"LIFO"方式,也就是说每次从队尾取出任务来执行。 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
join 检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞。 查看任务的完成状态,如果已经完成,直接返回结果。 如果任务尚未完成,但处于自己的工作队列内,则完成它。 如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以 FIFO 方式),执行,以期帮助它早日完成欲 join 的任务。 如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 join 的任务时,则找到小偷的小偷,帮助它完成它的任务。 递归地执行第5步。
# 1.13 线程池工作流程
1、如果线程池中的线程小于corePoolSize时就会创建新线程直接执行任务。 2、如果线程池中的线程大于corePoolSize时就会暂时把任务存储到工作队列workQueue中等待执行。 3、如果工作队列workQueue也满时,当线程数小于最大线程池数maximumPoolSize时就会创建新线程来处理,而线程数大于等于最大线程池数maximumPoolSize时就会执行拒绝策略。
# 1.14 线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。 当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。 使得在线程不至于一直占用cpu资源。
(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下 while (task != null || (task = getTask()) != null) {})。
不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?
# 1.15 线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
taskCount:线程池需要执行的任务数量。 completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。 largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。 getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。 getActiveCount:获取活动的线程数。 通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }
# 二、线程池常见面试题
# 2.1 线程池有哪几种实现方式?
Executors.newFixedThreadPool:创建固定线程数的线程池。核心线程数等于最大线程数,不存在空闲线程,keepAliveTime为0。 Executors.newSingleThreadExecutor:创建单线程的线程池,核心线程数和最大线程数都为1,相当于串行执行。 Executors.newScheduledThreadPool:创建支持定时以及周期性任务执行的线程池。最大线程数是Integer.MAX_VALUE。存在OOM风险。keepAliveTime为0,所以不回收工作线程。 Executors.newCachedThreadPool:核心线程数为0,最大线程数为Integer.MAX_VALUE,是一个高度可伸缩的线程池。存在OOM风险。keepAliveTime为60,工作线程处于空闲状态超过keepAliveTime会回收线程。 Executors.newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争。
# 2.2 禁止直接使用Executors创建线程池原因:
Executors.newCachedThreadPool
和Executors.newScheduledThreadPool
两个方法最大线程数为Integer.MAX_VALUE
,如果达到上限,没有任务服务器可以继续工作,肯定会抛出OOM异常。
Executors.newSingleThreadExecutor
和Executors.newFixedThreadPool
两个方法的workQueue参数为new LinkedBlockingQueue<Runnable>()
,容量为Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM风险。
# 2.3 线程池状态
线程池有这几个状态:RUNNING
,SHUTDOWN
,STOP
,TIDYING
,TERMINATED。
//线程池状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
2
3
4
5
6
- RUNNING 该状态的线程池会接收新任务,并处理阻塞队列中的任务; 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态; 调用线程池的shutdownNow()方法,可以切换到STOP状态;
- SHUTDOWN 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务; 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;
- STOP 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务; 线程池中执行的任务为空,进入TIDYING状态;
- TIDYING 该状态表明所有的任务已经运行终止,记录的任务数量为0。 terminated()执行完毕,进入TERMINATED状态
- TERMINATED 该状态表示线程池彻底终止
# 2.4 更多
面试问题1:Java的线程池说一下,各个参数的作用,如何进行的? 面试问题2:按线程池内部机制,当提交新任务时,有哪些异常要考虑。 面试问题3:线程池都有哪几种工作队列? 面试问题4:使用无界队列的线程池会导致内存飙升吗? 面试问题5:说说几种常见的线程池及使用场景?
# 三、线程池上下文传递
哪些情况需要用到线程池上下文传递呢?
主要针对于通过ThreadLocal传递数据的场景。
对于普通的ThreadLocal而言,其可见性为当前线程,如果把任务提交到线程池中执行,那么显然是读取不到提交任务所在线程的数据的。
常见的几种解决方案:
- 提交的时候直接进行显示的数据拷贝
- 如果是传递给子线程,那么可以使用InheritThreadLocal
- 对于线程池,可以使用TTL
# 四、动态线程池治理
所谓的动态线程池治理,其核心在于动态调整线程池的参数、监控统计线程池的指标。
现在有两个比较热门的开源动态线程池Java库,我们可以进行参考,也可以直接依赖使用。
# 五、线程池的进阶调优
对于线程池的进阶调优,一般涉及到几个层面:
- 核心线程数、队列大小、最大线程数这几个参数
- 常见开源框架对线程池的封装与增强
第三方库对线程池的增强不在少数: 以Tomcat为例,其实用的线程池是自身进行改造封装的,提交流程与JDK自身的线程池也不太一样,主要针对的就是web服务做的优化。