概述
线程可认为是操作系统可调度的最小的程序执行序列,一般作为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。在单核处理器架构下,操作系统一般使用分时的方式实现多线程;在多核处理器架构下,多个线程能够做到真正的在不同处理核心并行处理。
无论使用何种方式实现多线程,正确使用多线程都可以提高程序性能,或是吞吐量,或是响应时间,甚至两者兼具。如何正确使用多线程涉及较多的理论及最佳实践,本文无法详细展开,可参考如《Programming Concurrency on the JVM》等书籍。
本文主要内容为简单总结Java中线程池的相关信息。
Java线程使用及特点
Java中提供Thread
作为线程实现,一般有两种方式:
- 直接集成
Thread
类:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
class Starter{
public static void main(){
PrimeThread p = new PrimeThread(143);
p.start();
}
}
- 实现
Runnable
接口:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
class Starter{
public static void main(){
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
}
}
线程是属于操作系统的概念,Java中的多线线程实现一定会依托于操作系统支持。HotSpot虚拟机中对多线程的实现实际上是使用了一对一的映射模型,即一个Java进程映射到一个轻量级进程(LWP)之中。在使用Thread
的start
方法后,HotSpot创建本地线程并与Java线程关联。在此过程之中虚拟机需要创建多个对象(如OSThread
等)用于跟踪线程状态,后续需要进行线程初始化工作(如初始换ThreadLocalAllocBuffer
对象等),最后启动线程调用上文实现的run
方法。
由此可见创建线程的成本较高,如果线程中run
函数中业务代码执行时间非常短且消耗资源较少的情况下,可能出现创建线程成本大于执行真正业务代码的成本,这样难以达到提升程序性能的目的。
由于创建线程成本较大,很容易想到通过复用已创建的线程已达到减少线程创建成本的方法,此时线程池就可以发挥作用。
Java线程池
Java线程池主要核心类(接口)为Executor
,ExecutorService
,Executors
等,具体关系如下图所示:
Executor
接口
由以上类图可见在线程池类结构体系中Executor
作为最初始的接口,该接口仅仅规定了一个方法void execute(Runnable command)
,此接口作用为规定线程池需要实现的最基本方法为可运行实现了Runnable
接口的任务,并且开发人员不需要关心具体的线程池实现(在实际使用过程中,仍需要根据不同任务特点选择不同的线程池实现),将客户端代码与运行客户端代码的线程池解耦。
ExecutorService
接口
Executor
接口虽然完成了业务代码与线程池的解耦,但没有提供任何与线程池交互的方法,并且仅仅支持没有任何返回值的Runnable
任务的提交,在实际业务实现中功能略显不足。为了解决以上问题,JDK中增加了扩展Executor
接口的子接口ExecutorService
。 ExecutorService
接口主要在两方面扩展了Executor
接口:
- 提供针对线程池的多个管理方法,主要包括停止任务提交、停止线程池运行、判断线程池是否停止运行及线程池中任务是否运行完成;
- 增加
submit
的多个重载方法,该方法可在提交运行任务时,返回给提交任务的线程一个Future
对象,可通过该对象对提交的任务进行控制,如取消任务或获取任务结果等(Future对象如何实现此功能另行讨论
)。
Executors
工具类
Executors
是主要为了简化线程池的创建而提供的工具类,通过调用各静态工具方法返回响应的线程池实现。通过对其方法的观察可将其提供的工具方法归为如下几类:
- 创建
ExecutorService
对象的工具:又可细分为创建FixedThreadPool
、SingleThreadPool
、CachedThreadPool
、WorkStealingPool
、UnconfigurableExecutorService
、SingleThreadScheduledExecutor
及ThreadScheduledExecutor
; - 创建
ThreadFactory
对象; - 将
Runnable
等对象封装为Callable
对象。
以上各工具方法中使用最广泛的为newCachedThreadPool
、newFixedThreadPool
及newSingleThreadExecutor
,这三个方法创建的ExecutorService
对象均是其子类ThreadPoolExecutor
(严格来说newSingleThreadExecutor
方法返回的是FinalizableDelegatedExecutorService
对象,其封装了ThreadPoolExecutor
,为何如此实现后文在做分析),下文着重分析ThreadPoolExecutor
类。至于其他ExecutorService
实现类,如ThreadScheduledExecutor
本文不做详细分析。
ThreadPoolExecutor
类
ThreadPoolExecutor
类是线程池ExecutorService
的重要实现类,在工具类Executors
中构建的线程池对象,有大部分均是ThreadPoolExecutor
实现。 ThreadPoolExecutor
类提供多个构造参数对线程池进行配置,代码如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
现在对各个参数作用进行总结:
参数名称 | 参数类型 | 参数用途 |
---|---|---|
corePoolSize | int | 核心线程数,线程池中会一直保持该数量的线程,即使这些线程是空闲的状态,如果设置allowCoreThreadTimeOut 属性(默认为false)为true,则空闲超过超时时间的核心线程可以被回收 |
maximumPoolSize | int | 最大线程数,当前线程池中可存在的最大线程数 |
keepAliveTime | long | 线程存活时间,当当前线程池中线程数大于核心线程数时,空闲线程等待新任务的时间,超过该时间则停止空闲线程 |
unit | TimeUnit | 时间单位,keepAliveTime 属性的时间单位 |
workQueue | BlockingQueue<Runnable> | 等待队列,存储待执行的任务 |
threadFactory | ThreadFactory | 线程工厂,线程池创建线程时s使用 |
handler | RejectedExecutionHandler | 拒绝执行处理器,当提交任务被拒绝(当等待队列满,且线程达到最大限制后)时调用 |
在使用该线程池时有一个重要的参数起效顺序:
- 提交任务时,当当前运行的线程数小于核心线程时,则启动新的线程执行任务;
- 提交任务时,当前运行线程数大于等于核心线程数,将当前任务加入等待队列中;
- 将任务添加到等待队列失败时(如队列满),尝试新建线程运行任务;
- 新建线程时,线程池关闭或达到最大线程数,则拒绝任务,调用
handler
进行处理。
ThreadFactory
有默认的实现为Executors.DefaultThreadFactory
,其创建线程主要额外工作为将新建的线程加入当前线程组,并且将线程的名称置为pool-x-thread-y
的形式。
ThreadPoolExecutor
类通过内部类的形式提供了四种任务被拒绝时的处理器:AbortPolicy
、CallerRunsPolicy
、DiscardOldestPolicy
及DiscardPolicy
。
拒绝策略类 | 具体操作 |
---|---|
AbortPolicy |
抛出RejectedExecutionException 异常,拒绝执行任务 |
CallerRunsPolicy |
在提交任务的线程执行当前任务,即在调用函数execute 或submit 的线程直接运行任务 |
DiscardOldestPolicy |
直接取消当前等待队列中最早的任务 |
DiscardPolicy |
以静默方式丢弃任务 |
ThreadPoolExecutor
默认使用的是AbortPolicy
处理策略,用户可自行实现RejectedExecutionHandler
接口自定义处理策略,本处不在赘述。
Executors对于ThreadPoolExecutor的创建
根据上文描述,Executors
类提供了较多的关于创建或使用线程池的工具方法,此节重点总结其在创建ThreadPoolExecutor
线程池的各方法。
newCachedThreadPool
方法簇
newCachedThreadPool
方法簇用于创建可缓存任务的ThreadPoolExecutor
线程池。包括两个重构方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
结合上文分析的ThreadPoolExecutor
各构造参数,可总结如下:
- 核心线程数为0:没有核心线程,即在没有任务运行时所有线程均会被回收;
- 最大线程数为
Integer.MAX_VALUE
,即线程池中最大可存在的线程为Integer.MAX_VALUE
,由于此值在通常情况下远远大于系统可新建的线程数,可简单理解为此线程池不限制最大可建的线程数,此处可出现逻辑风险,在提交任务时可能由于超过系统处理能力造成无法再新建线程时会出现OOM异常,提示无法创建新的线程; - 存活时间60秒:线程数量超过核心线程后,空闲60秒的线程将会被回收,根据第一条可知核心线程数为0,则本条表示所有线程空闲超过60秒均会被回收;
- 等待队列
SynchronousQueue
:构建CachedThreadPool
时,使用的等待队列为SynchronousQueue
类型,此类型的等待队列较为特殊,可认为这是一个容量为0的阻塞队列,在调用其offer
方法时,如当前有消费者正在等待获取元素,则返回true
,否则返回false
。使用此等待队列可做到快速提交任务到空闲线程,没有空闲线程时触发新建线程; -
ThreadFactory
参数:默认为DefaultThreadFactory
,也可通过构造函数设置。
newFixedThreadPool
方法簇
newFixedThreadPool
方法簇用于创建固定线程数的ThreadPoolExecutor
线程池。包括两个构造方法:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
各构造参数总结:
- 核心线程数与最大线程数
nThreads
:构建的ThreadPoolExecutor
核心线程数与最大线程数相等且均为nThreads
,这说明当前线程池不会存在非核心线程,即不会存在线程的回收(allowCoreThreadTimeOut
默认为false
),随着任务的提交,线程数增加到nThreads
个后就不会变化; - 存活时间为0:线程存在非核心线程,该时间没有特殊效果;
- 等待队列
LinkedBlockingQueue
:该等待队列为LinkedBlockingQueue
类型,没有长度限制; -
ThreadFactory
参数:默认为DefaultThreadFactory,也可通过构造函数设置。
newSingleThreadExecutor
方法簇
newSingleThreadExecutor
方法簇用于创建只包含一个线程的线程池。包括两个构造方法:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
结合上文分析的ThreadPoolExecutor
各构造参数,可总结如下:
- 核心线程数与最大线程数1:当前线程池中有且仅有一个核心线程;
- 存活时间为0:当前线程池不存在非核心线程,不会存在线程的超时回收;
- 等待队列
LinkedBlockingQueue
:该等待队列为LinkedBlockingQueue
类型,没有长度限制; -
ThreadFactory
参数:默认为DefaultThreadFactory,也可通过构造函数设置。
特殊说明,函数实际返回的对象类型并不是ThreadPoolExecutor
而是FinalizableDelegatedExecutorService
类型,为何如此设计在后文统一讨论。
三种常见线程池的对比
上文总结了Executors
工具类创建常见线程池的方法,现对三种线程池区别进行比较。
线程池类型 | CachedThreadPool | FixedThreadPool | SingleThreadExecutor |
---|---|---|---|
核心线程数 | 0 |
nThreads (用户设定) |
1 |
最大线程数 | Integer.MAX_VALUE |
nThreads (用户设定) |
1 |
非核心线程存活时间 | 60s | 无非核心线程 | 无非核心线程 |
等待队列最大长度 | 1 | 无限制 | 无限制 |
特点 | 提交任务优先复用空闲线程,没有空闲线程则创建新线程 | 固定线程数,等待运行的任务均放入等待队列 | 有且仅有一个线程在运行,等待运行任务放入等待队列,可保证任务运行顺序与提交顺序一直 |
内存溢出 | 大量提交任务后,可能出现无法创建线程的OOM | 大量提交任务后,可能出现内存不足的OOM | 大量提交任务后,可能出现内存不足的OOM |
三种类型的线程池与GC关系
原理说明
一般情况下JVM中的GC根据可达性分析确认一个对象是否可被回收(eligible for GC),而在运行的线程被视为‘GCRoot’。因此被在运行的线程引用的对象是不会被GC回收的。在ThreadPoolExecutor
类中具有f非静态内部类Worker
,用于表示x当前线程池中的线程,并且根据Java语言规范An instance i of a direct inner class C of a class or interface O is associated with an instance of O, known as the immediately enclosing instance of i. The immediately enclosing instance of an object, if any, is determined when the object is created (§15.9.2).
可知非静态内部类对象具有外部包装类对象的引用(此处也可通过查看字节码来验证),因此Worker
类的对象即作为线程对象(‘GCRoot’)有持有外部类ThreadPoolExecutor
对象的引用,则在其运行结束之前,外部内不会被Gc回收。
根据以上分析,再次观察以上三个线程池:
- CachedThreadPool:没有核心线程,且线程具有超时时间,可见在其引用消失后,等待任务运行结束且所有线程空闲回收后,GC开始回收此线程池对象;
- FixedThreadPool:核心线程数及最大线程数均为
nThreads
,并且在默认allowCoreThreadTimeOut
为false
的情况下,其引用消失后,核心线程即使空闲也不会被回收,故GC不会回收该线程池; - SingleThreadExecutor:默认与
FixedThreadPool
情况一致,但由于其语义为单线程线程池,JDK开发人员为其提供了FinalizableDelegatedExecutorService
包装类,在创建FixedThreadPool
对象时实际返回的是FinalizableDelegatedExecutorService
对象,该对象持有FixedThreadPool
对象的引用,但FixedThreadPool
对象并不引用FinalizableDelegatedExecutorService
对象,这使得在FinalizableDelegatedExecutorService
对象的外部引用消失后,GC将会对其进行回收,触发finalize
函数,而该函数仅仅简单的调用shutdown
函数关闭线程,是的所有当前的任务执行完成后,回收线程池中线程,则GC可回收线程池对象。
因此可得出结论,CachedThreadPool
及SingleThreadExecutor
的对象在不显式调用shutdown
函数(或shutdownNow
函数),且其对象引用消失的情况下,可以被GC回收;FixedThreadPool
对象在不显式调用shutdown
函数(或shutdownNow
函数),且其对象引用消失的情况下不会被GC回收,会出现内存泄露。
实验验证
以上结论可使用实验验证:
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
//ExecutorService executorService = Executors.newFixedThreadPool(1);
//ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
//线程引用置空
executorService = null;
Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Shutdown.")));
//等待线程超时,主要对CachedThreadPool有效
Thread.sleep(100000);
//手动触发GC
System.gc();
}
使用以上代码,分别创建三种不同的线程池,可发现最终FixedThreadPool
不会打印出‘Shutdown.’,JVM没有退出。另外两种线程池均能退出JVM。
因此无论使用什么线程池线程池使用完毕后均调用shutdown
以保证其最终会被GC回收是一个较为安全的编程习惯。
猜想及踩坑代码示例
根据以上的原理及代码分析,很容易提出如下问题:既然SingleThreadExecutor
的实现方式可以自动完成线程池的关闭,为何不使用同样的方式实现FixedThreadPool
呢?
目前作者没有找到确切的原因,此处引用两个对此有所讨论的两个网址:王智超-理解SingleThreadExecutor及[Why doesn't all Executors factory methods wrap in a FinalizableDelegatedExecutorService?
](https://stackoverflow.com/que...。
作者当前提出一种不保证正确的可能性:JDK开发人员可能重语义方面考虑将FixedThreadPool
定义为可重新配置的线程池,SingleThreadExecutor
定义为不可重新配置的线程池。因此没有使用FinalizableDelegatedExecutorService
对象包装FixedThreadPool
对象,将其控制权放到了程序员手中。
最后再分享一个关于SingleThreadExecutor
的踩坑代码,改代码在编程过程中一般不会出现,但其中涉及较多知识点,不失为一个好的学习示例:
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
class Prog {
public static void main(String[] args) {
Callable<Long> callable = new Callable<Long>() {
public Long call() throws Exception {
// Allocate, to create some memory pressure.
byte[][] bytes = new byte[1024][];
for (int i = 0; i < 1024; i++) {
bytes[i] = new byte[1024];
}
return 42L;
}
};
for (;;) {
Executors.newSingleThreadExecutor().submit(callable);
}
}
}
以上代码在设置-Xmx128m
的虚拟机进行运行,大概率会抛出RejectedExecutionException
异常,其原理与上文分析的GC回收有关,详细分析可参考[Learning from bad code
](https://www.farside.org.uk/20...。
Executors对于ThreadPoolExecutor的创建的最佳实践
以上总结了使用Executors
创建常见线程池的方法,在简单的使用中的确方便使用且减少的手动创建线程池的代码量,但在真正开发高并发程序时,其默认创建的线程由于屏蔽了底层参数,程序员难以真正理解其中可能出现的细节问题,包括内存溢出及拒绝策略等,故在使用中t推荐使用ThreadPoolExecutor
等方式直接创建。此处可以参考《阿里巴巴Java开发手册终极版v1.3.0》(六)并发处理的第4点。
总结
本文简单总结了Java线程及常用线程池的使用,对比常见线程池的特点。由于本文侧重于分析使用层面,并没有深入探究各线程池具体的代码实现,此项可留后续继续补充。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。