资讯专栏INFORMATION COLUMN

Java线程池简单总结

CoorChice / 898人阅读

摘要:本文主要内容为简单总结中线程池的相关信息。方法簇方法簇用于创建固定线程数的线程池。三种常见线程池的对比上文总结了工具类创建常见线程池的方法,现对三种线程池区别进行比较。

概述

线程可认为是操作系统可调度的最小的程序执行序列,一般作为进程的组成部分,同一进程中多个线程可共享该进程的资源(如内存等)。在单核处理器架构下,操作系统一般使用分时的方式实现多线程;在多核处理器架构下,多个线程能够做到真正的在不同处理核心并行处理。
无论使用何种方式实现多线程,正确使用多线程都可以提高程序性能,或是吞吐量,或是响应时间,甚至两者兼具。如何正确使用多线程涉及较多的理论及最佳实践,本文无法详细展开,可参考如《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)之中。在使用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接口:

提供针对线程池的多个管理方法,主要包括停止任务提交、停止线程池运行、判断线程池是否停止运行及线程池中任务是否运行完成;

增加submit的多个重载方法,该方法可在提交运行任务时,返回给提交任务的线程一个Future对象,可通过该对象对提交的任务进行控制,如取消任务或获取任务结果等(Future对象如何实现此功能另行讨论)。

Executors工具类

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

创建ExecutorService对象的工具:又可细分为创建FixedThreadPoolSingleThreadPoolCachedThreadPoolWorkStealingPoolUnconfigurableExecutorServiceSingleThreadScheduledExecutorThreadScheduledExecutor

创建ThreadFactory对象;

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 workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)

现在对各个参数作用进行总结:

参数名称 参数类型 参数用途
corePoolSize int 核心线程数,线程池中会一直保持该数量的线程,即使这些线程是空闲的状态,如果设置allowCoreThreadTimeOut属性(默认为false)为true,则空闲超过超时时间的核心线程可以被回收
maximumPoolSize int 最大线程数,当前线程池中可存在的最大线程数
keepAliveTime long 线程存活时间,当当前线程池中线程数大于核心线程数时,空闲线程等待新任务的时间,超过该时间则停止空闲线程
unit TimeUnit 时间单位,keepAliveTime属性的时间单位
workQueue BlockingQueue 等待队列,存储待执行的任务
threadFactory ThreadFactory 线程工厂,线程池创建线程时s使用
handler RejectedExecutionHandler 拒绝执行处理器,当提交任务被拒绝(当等待队列满,且线程达到最大限制后)时调用

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

提交任务时,当当前运行的线程数小于核心线程时,则启动新的线程执行任务;

提交任务时,当前运行线程数大于等于核心线程数,将当前任务加入等待队列中;

将任务添加到等待队列失败时(如队列满),尝试新建线程运行任务;

新建线程时,线程池关闭或达到最大线程数,则拒绝任务,调用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());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue(),
                                    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());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue(),
                                    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()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue(),
                                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,并且在默认allowCoreThreadTimeOutfalse的情况下,其引用消失后,核心线程即使空闲也不会被回收,故GC不会回收该线程池;

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 callable = new Callable() {
      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线程及常用线程池的使用,对比常见线程池的特点。由于本文侧重于分析使用层面,并没有深入探究各线程池具体的代码实现,此项可留后续继续补充。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77197.html

相关文章

  • Java并发编程笔记(一)

    摘要:并发编程实战水平很高,然而并不是本好书。一是多线程的控制,二是并发同步的管理。最后,使用和来关闭线程池,停止其中的线程。当线程调用或等阻塞时,对这个线程调用会使线程醒来,并受到,且线程的中断标记被设置。 《Java并发编程实战》水平很高,然而并不是本好书。组织混乱、长篇大论、难以消化,中文翻译也较死板。这里是一篇批评此书的帖子,很是贴切。俗话说:看到有这么多人骂你,我就放心了。 然而知...

    cnsworder 评论0 收藏0
  • 美团面试题:Java-线程 ThreadPool 专题详解

    摘要:去美团面试,问到了什么是线程池,如何使用,为什么要用以下做个总结。二线程池线程池的作用线程池作用就是限制系统中执行线程的数量。真正的线程池接口是。创建固定大小的线程池。此线程池支持定时以及周期性执行任务的需求。 去美团面试,问到了什么是线程池,如何使用,为什么要用,以下做个总结。关于线程之前也写过一篇文章《高级面试题总结—线程池还能这么玩?》 1、什么是线程池:  java.util...

    enrecul101 评论0 收藏0
  • 美团面试题:Java-线程 ThreadPool 专题详解

    摘要:去美团面试,问到了什么是线程池,如何使用,为什么要用以下做个总结。二线程池线程池的作用线程池作用就是限制系统中执行线程的数量。真正的线程池接口是。创建固定大小的线程池。此线程池支持定时以及周期性执行任务的需求。 去美团面试,问到了什么是线程池,如何使用,为什么要用,以下做个总结。关于线程之前也写过一篇文章《高级面试题总结—线程池还能这么玩?》 1、什么是线程池:  java.util...

    wujl596 评论0 收藏0
  • Java SDK 并发包全面总结

    摘要:一和并发包中的和主要解决的是线程的互斥和同步问题,这两者的配合使用,相当于的使用。写锁与读锁之间互斥,一个线程在写时,不允许读操作。的注意事项不支持重入,即不可反复获取同一把锁。没有返回值,也就是说无法获取执行结果。 一、Lock 和 Condition Java 并发包中的 Lock 和 Condition 主要解决的是线程的互斥和同步问题,这两者的配合使用,相当于 synchron...

    luckyyulin 评论0 收藏0
  • Java 总结

    摘要:中的详解必修个多线程问题总结个多线程问题总结有哪些源代码看了后让你收获很多,代码思维和能力有较大的提升有哪些源代码看了后让你收获很多,代码思维和能力有较大的提升开源的运行原理从虚拟机工作流程看运行原理。 自己实现集合框架 (三): 单链表的实现 自己实现集合框架 (三): 单链表的实现 基于 POI 封装 ExcelUtil 精简的 Excel 导入导出 由于 poi 本身只是针对于 ...

    caspar 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<