Java线程池相关

82 阅读20分钟

0 线程池的优势

  • 通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊线程创建和销毁产生的巨大的开销;
  • 当请求到达时,通常工作线程已经存在,提高了响应性;
  • 通过配置线程池的大小,可以创建足够多的线程使CPU达到忙碌状态,还可以防止线程太多耗尽计算机的资源;

1 常用线程池类型

JDK通过Executors提供4种线程池,都是直接或间接继承自ThreadPoolExcecutor 线程池类。都是通过配置ThreadPoolExecutor的不同参数,来达到不同的线程管理效果。 image.png

newCachedThreadPool

创建一个可缓存的线程池,先查看池中有没有以前建立的线程,如果有,就reuse,如果没有,就建一个新的线程加入池中;当需求增加时,会增加线程数量;线程池规模无限制。

  • 缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不多。
  • 能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
  • 放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。

newFixedThreadPool

创建一个固定长度的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。当到达线程最大数量时,线程池的规模将不再变化。与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程,因为任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子;

  • 和cacheThreadPool不同,FixedThreadPool没有IDLE机制,所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器;
  • 从方法的源代码看,cache池和fixed 池调用的是同一个底层池,只不过参数不同: fixed池线程数固定,并且是0秒IDLE(无IDLE);cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE 。

newSingleThreadPoolExecutor

单例线程,任意时间池中只能有一个线程,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  • 用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

newScheduledThreadPool

调度型线程池:这是一个计划线程池类,此线程池支持定时以及周期性执行任务的需求。它能设置线程执行的先后间隔及执行时间等,功能比上面的三个强大了一些。

2 Executor 框架

2.1 简介

Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Threadstart 方法更好,更易管理,效率更好(用线程池实现,节约开销)。Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

2.2 Executor 框架结构(主要由三大部分组成)

1) 任务(Runnable /Callable)

执行任务需要实现的 Runnable 接口Callable接口Runnable 接口Callable 接口 实现类都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor 执行。

2) 任务的执行(Executor)

包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutorScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口image.png

3) 异步计算的结果(Future)

Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。当我们把 Runnable接口Callable 接口 的实现类提交给 ThreadPoolExecutorScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象)

2.3 Executor 框架的使用示意图

image.png

  1. 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
  2. 把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable <T> task))。
  3. 如果执行 ExecutorService.submit(…)ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
  4. 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

3 线程池的选择

1中的几种方法的源码中调用的都是同一个接口ThreadPoolExecutor方法,只不过传入参数不一样而已。而且,Executors 里面定义的 4 个线程池各自都存在一些问题,一般不建议使用。当然,如果没什么并发量,任务也不复杂也可以用。故而本质上,JDK(8) 里面只有三个线程池: ThreadPoolExecutor; ScheduledThreadPoolExecutor; ForkJoinPool。 所以推荐通过以上3种方法之一创建线程池,这样写线程更灵活。

3.1 线程池的选择

线程池的选择问题,其实就是 ThreadPoolExecutor,ScheduledThreadPoolExecutor,ForkJoinPool 三选一的问题。ThreadPoolExecutor 是应用面最广的,能应付大多数情况。下面探讨一下什么情况使用 ScheduledThreadPoolExecutor 或 ForkJoinPool 能带来压倒性优势。

3.1.1 ThreadPoolExecutor

  1. 它构造函数的一些基本参数如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
 
    //运行状态标志位
    volatile int runState;
    static final int RUNNING    = 0;
    static final int SHUTDOWN   = 1;
    static final int STOP       = 2;
    static final int TERMINATED = 3;
 
    //线程缓冲队列,当线程池线程运行超过一定线程时并满足一定的条件,待运行的线程会放入到这个队列
    private final BlockingQueue<Runnable> workQueue;
    //重入锁,更新核心线程池大小、最大线程池大小时要加锁
    private final ReentrantLock mainLock = new ReentrantLock();
    //重入锁状态
    private final Condition termination = mainLock.newCondition();
    //工作都set集合
    private final HashSet<Worker> workers = new HashSet<Worker>();
    //线程执行完成后在线程池中的缓存时间
    private volatile long  keepAliveTime;
    //核心线程池大小 
    private volatile int   corePoolSize;
    //最大线程池大小 
    private volatile int   maximumPoolSize;
    //当前线程池在运行线程大小 
    private volatile int   poolSize;
    //当缓冲队列也放不下线程时的拒绝策略
    private volatile RejectedExecutionHandler handler;
    //线程工厂,用来创建线程
    private volatile ThreadFactory threadFactory;   
    //用来记录线程池中曾经出现过的最大线程数
    private int largestPoolSize;   
    //用来记录已经执行完毕的任务个数
    private long completedTaskCount;   
 }

类构造方法参数解析:

  • corePoolSize(线程池基本大小):线程池长期维持的线程数,即使线程处于空闲状态,也不会回收。当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
  • maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  • keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  • workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。最常见的3种队列类型: 直接交换:SynchronousQueue(没有队列作为缓存) 无界队列:LinkedBlockingQueue(不限制队列大小,如果处理速度低于列队添加的速度,会浪费内存) 有界队列:ArrayBlockingQueue(队列容量满了后,才会创建新的线程)
  • threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  • handler(线程饱和策略,即拒绝策略):当线程池和队列都满了,再加入线程会执行此策略。

3.1.2 推荐使用 ThreadPoolExecutor 构造函数创建线程池

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 Executors 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPoolScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

通过ThreadPoolExecutor构造函数实现(推荐) image.png

ThreadPoolExecutor Vs ScheduledThreadPoolExecutor

这两个线程池的比较很简单,因为 ScheduledThreadPoolExecutor 也就是多了定时调度功能的 ThreadPoolExecutor,所以只有涉及定时调度功能时才会用到 ScheduledThreadPoolExecutor。

3.1.3 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。  这个在实际项目中基本不会被用到,也不推荐使用,只需要简单了解一下它的思想即可。

3.1.4 ForkJoinPool

  1. ForkJoinPool构造函数
private ForkJoinPool(int parallelism,
                         ForkJoinWorkerThreadFactory factory,
                         UncaughtExceptionHandler handler,
                         int mode,
                         String workerNamePrefix)
  • parallelism:并行度(the parallelism level),默认情况下跟我们机器的 CPU 核心数保持一致,使用 Runtime.getRuntime().availableProcessors() 可以得到我们机器运行时可用的 CPU 核心数。
  • factory:创建新线程的工厂( the factory for creating new threads)。默认情况下使用 ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory。
  • handler:线程异常情况下的处理器(Thread.UncaughtExceptionHandler handler),在线程执行任务时对由于某些无法预料的错误而导致任务线程中断时,该处理器会进行一些处理,默认情况为 null。
  • asyncMode:在ForkJoinPool中,每一个工作线程都有一个独立的任务队列,asyncMode 表示工作线程内的任务队列是采用何种方式进行调度,可以是先进先出FIFO,也可以是后进先出 LIFO。如果为 true,则线程池中的工作线程则使用 先进先出方式 进行任务调度,默认情况下是false 也就是默认为 LIFO 后进先出。
  • workerNamePrefix:顾名思义,工作线程名称前缀 默认为 "ForkJoinPool-" + nextPoolId() + "-worker-"
  1. ForkJoinPool工作模式
  • ForkJoinPool 采用了 一个线程对应专属的一个工作队列,而非 ThreadPoolExecutor 的多个线程对应一个工作队列。即 线程与工作队列关系 由 多对一 变为 一对一。

1)工作窃取(work-stealing):该算法是指某个线程从其他队列里窃取任务来执行。

  • ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
  • 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,每次从队尾取出任务来执行。
  • 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务会从队首窃取,也就是说窃取任务和执行自己的队列任务的方式是分开的,这样可以尽可能的避免竞争。 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
  • 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
  1. 使用ForkJoinPool的步骤:
  • 声明 ForkJoinPool 。
  • 继承实现 ForkJoinTask 抽象类或其子类,在其定义的方法中实现你的业务逻辑。
  • 子任务逻辑内部在合适的时机进行子任务的 fork 拆分。
  • 子任务逻辑内部在合适的时机进行 join 汇总

总结:整体上 ForkJoinPool 是对 ThreadPoolExecutor 的一种补充;ForkJoinPool 提供了其独特的线程工作队列绑定方式、工作分离以及窃取方式;ForkJoinPool + ForkJoinTask 配合实现了 Fork/Join 框架;适用于任务可拆分为更小的子任务的场景(有点类似递归),适用于计算密集型任务,可以充分发挥 CPU 多核的能力

ThreadPoolExecutor Vs ForkJoinPool

从功能上,ForkJoinPool 比 ThreadPoolExecutor 多了拆分子任务的功能,如果用 ThreadPoolExecutor 需要自己处理任务的拆分与合并,稍微麻烦一些。这里面,ForkJoinPool 的性能是不确定的,跟怎么拆分任务关系很大。所以ForkJoinPool 更大的意义在于实现分治更方便,如果用 ThreadPoolExecutor 需要代码实现任务拆分与结果合并。至于性能上的优势,不太明显,甚至有时候效率可能还要稍差些。

如果从两者的实现上定性地分析:

  • ThreadPoolExecutor 所有线程共用一个队列,可能存在竞争;而 ForkJoinPool 每个线程都从自己队列取任务,没有竞争;看似 ForkJoinPool 更占优势,但实际上存在一个问题,究竟需要多少个线程并发从一个阻塞队列取任务,才会出现线程竞争的情况?BlockingQueue 是基于 AQS 实现的,性能还是不错的,就算竞争可能也只是自旋几次。
  • ThreadPoolExecutor 很明确就是去固定的队列取任务;而 ForkJoinPool 有时候会扫描所有队列,寻找任务,反而可能更耗时一些;
  • ThreadPoolExecutor 先执行完的线程不会协助其它线程;而 ForkJoinPool 有任务窃取机制,可能存在一些优势,但如果任务拆分合理,各线程完成任务的耗时应该不会相差太多;
  • ForkJoinPool 要递归,所以要更频繁地创建栈帧、压栈、出栈,这绝对是一个劣势。 (参考链接:(blog.csdn.net/weixin_3838…)

3.2 实际应用中线程池的参数设计考虑

参数设计:参考:(blog.csdn.net/weixin_3838…)

4 线程池原理分析

4.1 线程池的工作流程

流程图

image.png 整个线程池启动一条线程的整体过程。总结: ThreadPoolExecutor中,包含了一个任务缓存队列和若干个执行线程,任务缓存队列是一个大小固定的缓冲区队列,用来缓存待执行的任务,执行线程用来处理待执行的任务。每个待执行的任务,都必须实现Runnable接口,执行线程调用其run()方法,完成相应任务。

  • ThreadPoolExecutor对象初始化时,不创建任何执行线程,当有新任务进来时,才会创建执行线程。
  • 构造ThreadPoolExecutor对象时,需要配置该对象的核心线程池大小和最大线程池大小:
  • 当目前执行线程的总数小于核心线程大小时,所有新加入的任务,都在新线程中处理
  • 当目前执行线程的总数大于或等于核心线程时,所有新加入的任务,都放入任务缓存队列中
  • 当目前执行线程的总数大于或等于核心线程,并且缓存队列已满,同时此时线程总数小于线程池的最大大小,那么创建新线程,加入线程池中,协助处理新的任务。
  • 当所有线程都在执行,线程池大小已经达到上限,并且缓存队列已满时,就rejectHandler拒绝新的任务

4.2 execute方法

1.源码

        if (command == null)
            throw new NullPointerException();
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { //  判断1
            if (runState == RUNNING && workQueue.offer(command)) { // 判断2
                if (runState != RUNNING || poolSize == 0)  //  判断3
                    ensureQueuedTaskHandled(command);
            }
            else if (!addIfUnderMaximumPoolSize(command)) //  判断4
                reject(command); // is shutdown or saturated
        }
    }

2.详细过程: 首先判断1,当poolSize >= corePoolSize 不成立时,表明当前线程数小于核心线程数目,左边返回fasle.接着执行右边判断!addIfUnderCorePoolSize(command),如果addIfUnderCorePoolSize返回true,刚表明成功添加一条线程,并调用了其start方法,那么整个调用到此结束,执行判断2。或者当poolSize >= corePoolSize成立时,整个判断返回true。接着执行判断2; 判断2,如果当前线程池在运行状态,并且将当前线程加入到缓冲队列中。workQueue的offer是一个非阻塞方法。如查缓冲队列满了的话,返回为false.否则返回true;如果上面两个都为true,表明线程被成功添加到缓冲队列中,并且当前线程池在运行。进入判断3; 判断3,当线程被加入到线程池中,进入判断3.如果这时线程池没有在运行或者运行的线程为为0。那么就调用ensureQueuedTaskHandled,它做的其实是判断下是否在拒绝这个线程的执行。 判断4,在判断2为false时执行,表明当前线程池没有在运行或者该线程加入缓冲队列中失败,那么就会尝试再启动下该线程,如果还是失败,那就根据拒绝策略来处理这个线程。一般调用addIfUnderMaximumPoolSize()这个方法是发生在缓冲队列已满了,那么线程池会尝试直接启动该线程。当然,它要保存当前运行的poolSize一定要小于maximumPoolSize。否则,最后。还是会拒绝这个线程!

4.3Runnable vs Callable

Callable 是JDK1.5时加入的接口,作为 Runnable 的一种补充,功能更强大了,允许有返回值,允许抛出异常。所以向线程池提交任务有两种方式:RunnableCallable,二者的区别如下:

  1. 方法签名不同,void Runnable.run()V Callable.call() throws Exception
  2. 是否允许有返回值,是否允许抛出异常。Callable允许有返回值且允许抛出异常。
  3. Calllable 支持范型的返回值,需要借助FutureTask类,比如获取返回结果

Runnable.java

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

提交任务的3种方式

void execute(Runnable command):执行任务命令,没有返回值,一般用来执行Runnable.
Future submit(Callable task):执行任务,有返回值,一般用来执行Callable image.png

execute() vs submit()

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException

最后:小细节

常见的拒绝策略

线程池提供了几种常见的拒绝策略(上面提到的 RejectedExecutionHandler): 这四种拒绝策略,在ThreadPoolExecutor是四个内部类。 image.png

  1. AbortPolicy:当任务添加到线程池中被拒绝时,直接丢弃任务,并抛出RejectedExecutionException异常。
  2. DiscardPolicy:当任务添加到线程池中被拒绝时,丢弃被拒绝的任务,不抛异常。
  3. DiscardOldestPolicy:当任务添加到线程池中被拒绝时,丢弃任务队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
  4. CallerRunsPolicy:被拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务。(总结:就是被拒绝的任务,直接在主线程中运行,不再进入线程池。)

线程池状态

  • RUNNING:接受新任务并处理排队任务
  • SHUTDOWN:不接受新任务,单处理排队任务
  • STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务(shutdownNow)
  • TIDYING:所有任务都已终止,(worderCount)任务数为0,线程会转到TIDYING状态,并执行terminate()钩子方法
  • TERMINATED:terminate()运行完成

如何合理配置线程池

使用线程池时通常我们可以将执行的任务分为两类:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,需要线程长时间进行的复杂的运算,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。而过多的线程将会频繁引起上文切换,降低任务处理速度。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用,增加线程数量可以提高并发度,尽可能多处理任务。。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

Future接口

一般情况下,使用Runnable接口、Thread实现的线程我们都是无法返回结果的。但是如果对一些场合需要线程返回的结果。就要使用用Callable、Future、FutureTask、CompletionService这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态。Future 本身也是一种设计模式,它是用来取得异步任务的结果。

  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等;

  • FutureTask是Future接口的唯一实现类;

  • FutureTask同时实现了Runnable,Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值;

    参考(blog.csdn.net/weixin_3838…)