线程可认为是操做系统可调度的最小的程序执行序列,通常做为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。在单核处理器架构下,操做系统通常使用分时的方式实现多线程;在多核处理器架构下,多个线程可以作到真正的在不一样处理核心并行处理。
不管使用何种方式实现多线程,正确使用多线程均可以提升程序性能,或是吞吐量,或是响应时间,甚至二者兼具。如何正确使用多线程涉及较多的理论及最佳实践,本文没法详细展开,可参考如《Programming Concurrency on the JVM》等书籍。
本文主要内容为简单总结Java中线程池的相关信息。html
Java中提供Thread
做为线程实现,通常有两种方式:java
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线程池主要核心类(接口)为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)
如今对各个参数做用进行总结:oracle
参数名称 | 参数类型 | 参数用途 |
---|---|---|
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
线程池的各方法。
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
各构造参数,可总结以下:
Integer.MAX_VALUE
,即线程池中最大可存在的线程为Integer.MAX_VALUE
,因为此值在一般状况下远远大于系统可新建的线程数,可简单理解为此线程池不限制最大可建的线程数,此处可出现逻辑风险,在提交任务时可能因为超过系统处理能力形成没法再新建线程时会出现OOM异常,提示没法建立新的线程;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
个后就不会变化;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
各构造参数,可总结以下:
LinkedBlockingQueue
:该等待队列为LinkedBlockingQueue
类型,没有长度限制;ThreadFactory
参数:默认为DefaultThreadFactory,也可经过构造函数设置。特殊说明,函数实际返回的对象类型并非ThreadPoolExecutor
而是FinalizableDelegatedExecutorService
类型,为什么如此设计在后文统一讨论。
上文总结了Executors
工具类建立常见线程池的方法,现对三种线程池区别进行比较。
线程池类型 | CachedThreadPool | FixedThreadPool | SingleThreadExecutor |
---|---|---|---|
核心线程数 | 0 | nThreads (用户设定) |
1 |
最大线程数 | Integer.MAX_VALUE | nThreads (用户设定) |
1 |
非核心线程存活时间 | 60s | 无非核心线程 | 无非核心线程 |
等待队列最大长度 | 1 | 无限制 | 无限制 |
特色 | 提交任务优先复用空闲线程,没有空闲线程则建立新线程 | 固定线程数,等待运行的任务均放入等待队列 | 有且仅有一个线程在运行,等待运行任务放入等待队列,可保证任务运行顺序与提交顺序一直 |
内存溢出 | 大量提交任务后,可能出现没法建立线程的OOM | 大量提交任务后,可能出现内存不足的OOM | 大量提交任务后,可能出现内存不足的OOM |
通常状况下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回收。
根据以上分析,再次观察以上三个线程池:
nThreads
,而且在默认allowCoreThreadTimeOut
为false
的状况下,其引用消失后,核心线程即便空闲也不会被回收,故GC不会回收该线程池;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
建立常见线程池的方法,在简单的使用中的确方便使用且减小的手动建立线程池的代码量,但在真正开发高并发程序时,其默认建立的线程因为屏蔽了底层参数,程序员难以真正理解其中可能出现的细节问题,包括内存溢出及拒绝策略等,故在使用中t推荐使用ThreadPoolExecutor
等方式直接建立。此处能够参考《阿里巴巴Java开发手册终极版v1.3.0》(六)并发处理的第4点。
本文简单总结了Java线程及经常使用线程池的使用,对比常见线程池的特色。因为本文侧重于分析使用层面,并无深刻探究各线程池具体的代码实现,此项可留后续继续补充。