Java线程池简单总结

概述

线程可认为是操做系统可调度的最小的程序执行序列,通常做为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。在单核处理器架构下,操做系统通常使用分时的方式实现多线程;在多核处理器架构下,多个线程可以作到真正的在不一样处理核心并行处理。
不管使用何种方式实现多线程,正确使用多线程均可以提升程序性能,或是吞吐量,或是响应时间,甚至二者兼具。如何正确使用多线程涉及较多的理论及最佳实践,本文没法详细展开,可参考如《Programming Concurrency on the JVM》等书籍。
本文主要内容为简单总结Java中线程池的相关信息。html

Java线程使用及特色

Java中提供Thread做为线程实现,通常有两种方式:java

  1. 直接集成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();
    }
}
  1. 实现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)之中。在使用Threadstart方法后,HotSpot建立本地线程并与Java线程关联。在此过程之中虚拟机须要建立多个对象(如OSThread等)用于跟踪线程状态,后续须要进行线程初始化工做(如初始换ThreadLocalAllocBuffer对象等),最后启动线程调用上文实现的run方法。
因而可知建立线程的成本较高,若是线程中run函数中业务代码执行时间很是短且消耗资源较少的状况下,可能出现建立线程成本大于执行真正业务代码的成本,这样难以达到提高程序性能的目的。
因为建立线程成本较大,很容易想到经过复用已建立的线程已达到减小线程建立成本的方法,此时线程池就能够发挥做用。程序员

Java线程池

Java线程池主要核心类(接口)为ExecutorExecutorServiceExecutors等,具体关系以下图所示:
编程

Executor接口

由以上类图可见在线程池类结构体系中Executor做为最初始的接口,该接口仅仅规定了一个方法void execute(Runnable command),此接口做用为规定线程池须要实现的最基本方法为可运行实现了Runnable接口的任务,而且开发人员不须要关心具体的线程池实现(在实际使用过程当中,仍须要根据不一样任务特色选择不一样的线程池实现),将客户端代码与运行客户端代码的线程池解耦。缓存

ExecutorService接口

Executor接口虽然完成了业务代码与线程池的解耦,但没有提供任何与线程池交互的方法,而且仅仅支持没有任何返回值的Runnable任务的提交,在实际业务实现中功能略显不足。为了解决以上问题,JDK中增长了扩展Executor接口的子接口ExecutorService
ExecutorService接口主要在两方面扩展了Executor接口:安全

  1. 提供针对线程池的多个管理方法,主要包括中止任务提交、中止线程池运行、判断线程池是否中止运行及线程池中任务是否运行完成;
  2. 增长submit的多个重载方法,该方法可在提交运行任务时,返回给提交任务的线程一个Future对象,可经过该对象对提交的任务进行控制,如取消任务或获取任务结果等(Future对象如何实现此功能另行讨论)。

Executors工具类

Executors是主要为了简化线程池的建立而提供的工具类,经过调用各静态工具方法返回响应的线程池实现。经过对其方法的观察可将其提供的工具方法归为以下几类:多线程

  1. 建立ExecutorService对象的工具:又可细分为建立FixedThreadPoolSingleThreadPoolCachedThreadPoolWorkStealingPoolUnconfigurableExecutorServiceSingleThreadScheduledExecutorThreadScheduledExecutor
  2. 建立ThreadFactory对象;
  3. Runnable等对象封装为Callable对象。

以上各工具方法中使用最普遍的为newCachedThreadPoolnewFixedThreadPoolnewSingleThreadExecutor,这三个方法建立的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 拒绝执行处理器,当提交任务被拒绝(当等待队列满,且线程达到最大限制后)时调用

在使用该线程池时有一个重要的参数起效顺序:

  1. 提交任务时,当当前运行的线程数小于核心线程时,则启动新的线程执行任务;
  2. 提交任务时,当前运行线程数大于等于核心线程数,将当前任务加入等待队列中;
  3. 将任务添加到等待队列失败时(如队列满),尝试新建线程运行任务;
  4. 新建线程时,线程池关闭或达到最大线程数,则拒绝任务,调用handler进行处理。

ThreadFactory有默认的实现为Executors.DefaultThreadFactory,其建立线程主要额外工做为将新建的线程加入当前线程组,而且将线程的名称置为pool-x-thread-y的形式。

ThreadPoolExecutor类经过内部类的形式提供了四种任务被拒绝时的处理器:AbortPolicyCallerRunsPolicyDiscardOldestPolicyDiscardPolicy

拒绝策略类 具体操做
AbortPolicy 抛出RejectedExecutionException异常,拒绝执行任务
CallerRunsPolicy 在提交任务的线程执行当前任务,即在调用函数executesubmit的线程直接运行任务
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各构造参数,可总结以下:

  1. 核心线程数为0:没有核心线程,即在没有任务运行时全部线程均会被回收;
  2. 最大线程数为Integer.MAX_VALUE,即线程池中最大可存在的线程为Integer.MAX_VALUE,因为此值在一般状况下远远大于系统可新建的线程数,可简单理解为此线程池不限制最大可建的线程数,此处可出现逻辑风险,在提交任务时可能因为超过系统处理能力形成没法再新建线程时会出现OOM异常,提示没法建立新的线程;
  3. 存活时间60秒:线程数量超过核心线程后,空闲60秒的线程将会被回收,根据第一条可知核心线程数为0,则本条表示全部线程空闲超过60秒均会被回收;
  4. 等待队列SynchronousQueue:构建CachedThreadPool时,使用的等待队列为SynchronousQueue类型,此类型的等待队列较为特殊,可认为这是一个容量为0的阻塞队列,在调用其offer方法时,如当前有消费者正在等待获取元素,则返回true,不然返回false。使用此等待队列可作到快速提交任务到空闲线程,没有空闲线程时触发新建线程;
  5. 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);
}

各构造参数总结:

  1. 核心线程数与最大线程数nThreads:构建的ThreadPoolExecutor核心线程数与最大线程数相等且均为nThreads,这说明当前线程池不会存在非核心线程,即不会存在线程的回收(allowCoreThreadTimeOut默认为false),随着任务的提交,线程数增长到nThreads个后就不会变化;
  2. 存活时间为0:线程存在非核心线程,该时间没有特殊效果;
  3. 等待队列LinkedBlockingQueue:该等待队列为LinkedBlockingQueue类型,没有长度限制;
  4. 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. 核心线程数与最大线程数1:当前线程池中有且仅有一个核心线程;
  2. 存活时间为0:当前线程池不存在非核心线程,不会存在线程的超时回收;
  3. 等待队列LinkedBlockingQueue:该等待队列为LinkedBlockingQueue类型,没有长度限制;
  4. 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回收。
根据以上分析,再次观察以上三个线程池:

  1. CachedThreadPool:没有核心线程,且线程具备超时时间,可见在其引用消失后,等待任务运行结束且全部线程空闲回收后,GC开始回收此线程池对象;
  2. FixedThreadPool:核心线程数及最大线程数均为nThreads,而且在默认allowCoreThreadTimeOutfalse的状况下,其引用消失后,核心线程即便空闲也不会被回收,故GC不会回收该线程池;
  3. SingleThreadExecutor:默认与FixedThreadPool状况一致,但因为其语义为单线程线程池,JDK开发人员为其提供了FinalizableDelegatedExecutorService包装类,在建立FixedThreadPool对象时实际返回的是FinalizableDelegatedExecutorService对象,该对象持有FixedThreadPool对象的引用,但FixedThreadPool对象并不引用FinalizableDelegatedExecutorService对象,这使得在FinalizableDelegatedExecutorService对象的外部引用消失后,GC将会对其进行回收,触发finalize函数,而该函数仅仅简单的调用shutdown函数关闭线程,是的全部当前的任务执行完成后,回收线程池中线程,则GC可回收线程池对象。

所以可得出结论,CachedThreadPoolSingleThreadExecutor的对象在不显式调用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线程及经常使用线程池的使用,对比常见线程池的特色。因为本文侧重于分析使用层面,并无深刻探究各线程池具体的代码实现,此项可留后续继续补充。

相关文章
相关标签/搜索