java线程池详解及源码解析

线程池详解

线程池参数

     public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

  • corePoolSize
    核心线程数,根据任务执行创建从0到corePoolSize制定的线程数,不会回收线程,始终保持
  • maximumPoolSize
    最大线程数,当前线程池最大线程容量,相当于corePoolSize+临时线程的数量,如果corePoolSize==maximumPoolSize,则当前只有核心线程;如果maximumPoolSize>corePoolSize时,当核心线程达到corePoolSize时,会创建临时线程来执行任务。通俗一点讲,corePoolSize是正式工,maximumPoolSize-corePoolSize是临时工。临时线程会随着任务执行完,等待keepAliveTime设置的时间之后进行回收
  • keepAliveTime
    空闲线程最大存活时间
  • workQueue
    用于存放Runnable任务的队列,根据不用场景,有下面几种常见阻塞队列:LinkedBlockingQueue(无上限)、ArrayBlockingQueue(固定上限,不扩容)、SynchronousQueue(无容量)、DelayedWorkQueue(延迟队列)
  • threadFactory
    线程工厂,用来创建线程,根据业务可以设置不通线程名来快速定位问题
  • handler
    拒绝策略,当线程池满了时,执行的策略,常见的有:
    AbortPolicy - 直接抛出RejectedExecutionException异常
    DiscardPolicy - 直接丢弃任务
    DiscardOldestPolicy - 丢弃最早的任务,然后重新执行线程池execute
    CallerRunsPolicy - 除非线程池已经关闭,否则直接执行任务

常用的六种线程池

FixedThreadPool

  public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
  }

从创建方法来看传入的是一个核心线程数和最大线程数相同的数量,意味着这个线程池只有核心线程运行。可以看做是固定线程的线程池。线程池中的线程数只在初始阶段从0开始增加,之后的线程数都是固定的,就算任务数超过了线程数,也不会创建新的线程,由于当前不会创建新线程,因此这里使用了一个无限容量的LinkedBlockingQueue队列来存储任务

CachedThreadPool

  public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
  }

可缓存线程池,它的特点是线程数几乎可以无限增加(设置了Integer.MAX_VALUE个最大线程数,但是由于内存上限原因,几乎不可能达到这个值,途中会发生OOM),而当线程闲置时还可以对线程进行回收。由于线程数可以无限增加,并不需要使用队列来存储任务,所以用了一个无容量的SynchronousQueue队列来存储,它的作用是对任务进行中转和传递,所以效率比较高。

ScheduledThreadPool

  public ScheduledThreadPoolExecutor(int corePoolSize) {
      super(corePoolSize, Integer.MAX_VALUE,
            DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
            new DelayedWorkQueue());
  }

定时或周期性执行任务。支持的使用方式:

  ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
  
  service.schedule(new Task(), 10, TimeUnit.SECONDS);
   
  service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
   
  service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);

三种方法的区别:

  • 第一种方法 schedule 比较简单,表示延迟指定时间后执行一次任务,如果代码中设置参数为 10 秒,也就是 10 秒后执行一次任务后就结束。
  • 第二种方法 scheduleAtFixedRate 表示以固定的频率执行任务,它的第二个参数 initialDelay 表示第一次延时时间,第三个参数 period 表示周期,也就是第一次延时后每次延时多长时间执行一次任务。
  • 第三种方法 scheduleWithFixedDelay 与第二种方法类似,也是周期执行任务,区别在于对周期的定义,之前的 scheduleAtFixedRate 是 以 任 务 开 始 的 时 间 为 时 间 起 点 开 始 计 时 \color{Coral}{以任务开始的时间为时间起点开始计时} ,时间到就开始执行第二次任务,而不管任务需要花多久执行;而 scheduleWithFixedDelay 方法 以 任 务 结 束 的 时 间 为 下 一 次 循 环 的 时 间 起 点 开 始 计 时 \color{Coral}{以任务结束的时间为下一次循环的时间起点开始计时}

由于支持定时任务,所以使用的是支持延时的DelayedWorkQueue延时队列,内部任务根据延迟时间先后排列。

SingleThreadExecutor

  public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()));
  }

它的特点是使用唯一线程去执行任务,原理和FixedThreadPool一样,只不过线程只有一个,因此也非常适合执行需要按顺序提交的任务。

SingleThreadScheduledExecutor

  new ScheduledThreadPoolExecutor(1)

和第三种 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程

几种线程池参数对比
参数FixedThreadPoolCachedThreadPoolScheduledThreadPoolSingleThreadExecutorSingleThreadScheduledExecutor
corePoolSize构造参数传入0构造参数传入11
maximumPoolSize同corePoolSizeInteger.MAX_VALUEInteger.MAX_VALUE1Integer.MAX_VALUE
keepAliveTime060000
workQueueLinkedBlockingQueueSynchronousQueueDelayedWorkQueueLinkedBlockingQueueDelayedWorkQueue

ForkJoinPool

这个线程池是JDK 7加入的,从名字就可以看出这个线程池适用于执行多个不相关的子任务的。

组成部分:

1.ForkJoinPool:充当fork/join框架里面的管理者,最原始的任务都要交给它才能处理。它负责控制整个fork/join有多少个workerThread,workerThread的创建,激活都是由它来掌控。它还负责workQueue队列的创建和分配,每当创建一个workerThread,它负责分配相应的workQueue。然后它把接到的活都交给workerThread去处理,它可以说是整个frok/join的容器。

2.ForkJoinWorkerThread:fork/join里面真正干活的"工人",本质是一个线程。里面有一个ForkJoinPool.WorkQueue的队列存放着它要干的活,接活之前它要向ForkJoinPool注册(registerWorker),拿到相应的workQueue。然后就从workQueue里面拿任务出来处理。它是依附于ForkJoinPool而存活,如果ForkJoinPool的销毁了,它也会跟着结束。

3.ForkJoinPool.WorkQueue: 双端队列就是它,它负责存储接收的任务。

4.ForkJoinTask:代表fork/join里面任务类型,我们一般用它的两个子类RecursiveTask、RecursiveAction。这两个区别在于RecursiveTask任务是有返回值,RecursiveAction没有返回值。任务的处理逻辑包括任务的切分都集中在compute()方法里面。

工作方式:

使用一种分治算法,递归地将任务分割成更小的子任务,其中阈值可配置,然后把子任务分配给不同的线程执行并发执行,最后再把结果组合起来。该用法常见于数组与集合的运算。

由于提交的任务不一定能够递归地分割成ForkJoinTask,且ForkJoinTask执行时间不等长,所以ForkJoinPool使用一种工作窃取的算法,允许空闲的线程“窃取”分配给另一个线程的工作。由于工作无法平均分配并执行。所以工作窃取算法能更高效地利用硬件资源。

使用场景:

适用于少量线程完成大量任务,一般用于非阻塞的,能快速处理的业务,或阻塞时延比较少的。

核心问题

拒绝策略

拒绝时机

线程池会在以下两种情况下会拒绝新提交的任务:

  • 第一种情况是当我们调用 shutdown 等方法关闭线程池后,即便此时可能线程池内部依然有没执行完的任务正在执行,但是由于线程池已经关闭,此时如果再向线程池内提交任务,就会遭到拒绝。
  • 第二种情况是线程池没有能力继续处理新提交的任务,也就是工作已经非常饱和的时候。

上面已经粗略的列举了常用的四种拒绝策略,下面就详细讲解

AbortPolicy
   public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //直接抛出异常
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。

DiscardPolicy
   public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          //不执行任何策略,直接丢弃
        }
    }

当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。

DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          //在线程池没有关闭的情况下,去除第一个丢弃,再尝试执行任务
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与DiscardPolicy不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。

CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          //如果线程池没有关闭,直接执行任务的run方法
          if (!e.isShutdown()) {
              r.run();
          }
        }
    }

当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。

  • 新提交的任务不会被丢弃,这样也就不会造成业务损失。
  • 由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

常用几种阻塞队列

线程池用于执行并发任务的,这涉及到多线程的同步操作,所以使用支持并发的BlockingQueue阻塞队列来存储任务,使用的队列都需要实现统一的接口方便线程池操作,下面就是常用的3种队列

LinkedBlockingQueue

在FixedThreadPool和SingleThreadExecutor中,使用了一个容量为Integer.MAX_VALUE的LinkedBlockingQueue,由于这两种线程池都使用了固定的线程数,此时就需要使用一个没有容量限制的容器来存储任务

SynchronousQueue

在CachedThreadPool中,最大线程数是Integer.MAX_VALUE,意味着线程数可以无限增加。这个时候就不需要有容量的队列来存储任务,一旦任务被提交就直接转发给线程或者新建线程来处理任务。

我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

DelayedWorkQueue

在ScheduledThreadPool和SingleThreadScheduledExecutor中,使用了DelayedWorkQueue队列,它最大的特点就是能延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。DelayedWorkQueue内部特点是元素并不是按照加入顺序排列,而是按照延迟的时间长度来对任务进行排序,采用的堆数据结构。

之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

为什么不应该自动创建线程池

阿里java开发规范里明确规定了不应该使用Executors来自动创建线程池,接下来将讲各个线程池可能出现的问题。

FixedThreadPool

该线程池是一个固定的线程数,使用无限容量队列LinkedBlockingQueue的线程池。

当我们对任务的处理速度比较慢,随着请求的增多,队列中的任务会越来越多,最终过多的任务占用大量的内存,导致OOM,这就影响到了整个程序,产生严重的后果。

SingleThreadExecutor

该线程和FixedThreadPool的问题一样,都是固定线程数的线程池,同样使用了无限容量队列LinkedBlockingQueue,只是线程数为1,会导致和FixedThreadPool相同的问题

CachedThreadPool

和上两种线程池不一样的是,CachedThreadPool是用了线程数为Integer.MAX_VALUE、无容量的SynchronousQueue队列,队列本身是没有任何问题的,问题出在线程数几乎无限大,创建大量的线程来执行任务时,由于操作系统的最大线程数是有限制的,最终会导致创建的线程数超过了操作系统的限制而无法创建线程,或者内存不足。

ScheduledThreadPool 和 SingleThreadScheduledExecutor

这两种线程池都是使用无界的DelayedWorkQueue,会和LinkedBlockingQueue一样,队列中存放过多的任务会导致OOM

合适的线程数

这个是面试中经常问到的一道题,我们调整线程池中的线程数量的最主要目的是为了充分并合理地利用CPU和内存等资源,从而最大限度的提高程序的性能。在实际工作中需要根据不同的业务来选择不同的策略。

CPU密集型任务

CPU密集型任务是指加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。

最 佳 线 程 数 = C P U 核 心 数 的 1 − 2 倍 \color{RED}{最佳线程数 = CPU 核心数的 1-2 倍} 线=CPU12

如果设置过多的线程,实际上并不会起到很好的效果。此时假设我们设置的线程数是 CPU 核心数的 2 倍以上,因为计算机的任务很重,会占用大量的 CPU 资源,所以这是 CPU 每个核心都是满负荷工作,而设置过多的线程数,每个线程都去抢占 CPU 资源,就会产生不必要的上下文切换,反而会造成整体性能的下降。

耗时 IO 型任务

耗时 IO 型任务是指数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。

对于这种情况任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

推荐计算公式
  线程数 = CPU核心数 *1 + IO耗时/CPU耗时)

通过这个公式,我们可以计算出一个合理的线程数量,如果任务的 IO 耗时时间长,线程数就随之增加,而如果CPU 耗时长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。

太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。

小结:

  • 线程的CPU耗时所占比例越高,就需要越少的线程;
  • 线程的IO耗时所占比例越高,就需要越多的线程;
  • 针对不同的程序,进行对应的实际测试就可以得到最合适的选择。

定制线程池

定制线程池主要就是设置创建线程池所需要的几个参数。

核心线程数

第一个设置的参数是核心线程数,在上面已经讲了,合理的线程数和任务类型,以及CPU核心数有着密切关系,基本结论是线程的CPU耗时所占比例越高,就设置越少的线程;线程的IO耗时所占比例越高,就设置越多的线程。

最大线程数

对于最大线程数,如果我们执行的任务类型不是固定的,比如可能一段时间是 CPU 密集型,另一段时间是 IO 密集型,或是同时有两种任务相互混搭。那么在这种情况下我们可以把最大线程数设置为核心线程数的几倍,以方便应对任务突发情况。当然,最好的是根据业务类型区分,而且不是混在一个线程池中,使用不同的线程池执行对应的不同任务。具体的线程数需要实际的压测和结果来设置合理的线程数。

阻塞队列

上面介绍的几种阻塞队列:

  • LinkedBlockingQueue
    用于线程数固定的情况下存储任务,区别于DelayedWorkQueue的是不支持延迟执行任务。

  • SynchronousQueue
    内部无容量,用于线程数很大,不需要存储任务的业务场景。

  • DelayedWorkQueue
    无界的对垒,较适用于延迟执行任务的业务场景

  • ArrayBlockingQueue
    除了自动创建使用的三种队列,其实ArrayBlockingQueue也常被用于线程池中。ArrayBlockingQueue内部是使用数组实现的,在新建的时候传入容量值,且后期不会扩容,所以ArrayBlockingQueue最大的特点就是有限的。这样一来,如果队列里放满了任务,而且线程数已经达到最大值,线程池就会根据拒绝策略拒绝新提交的任务,所以也会产生一定的数据丢失。

BlockingQueue的三组方法区别
组别方法含义特点总结
第一组add添加一个元素如果队列满了,操作失败,抛出IllegalStateException抛出异常
remove返回并删除队列的头元素如果队列为空,删除失败,抛出NoSuchElementException
element返回队列头元素如果队列为空,获取失败,抛出NoSuchElementException
第二组offer添加一个元素如果队列满了,返回false;如果添加成功,返回true返回结果但不抛出异常
poll返回并删除队列的头元素如果队列空,删除失败,返回null
peek返回队列头元素如果队列空,获取失败,返回null
第三组put添加一个元素如果队列满了,就会阻塞阻塞
take返回并删除队列的头元素如果队列空,就会阻塞
如何选择
-是否需要排序容量是否扩容内存结构性能
LinkedBlockingQueue-无限容量链表,空间利用低两把锁,相对高效
ArrayBlockingQueue-固定容量-数组,空间利用高一把锁,相对低效
PriorityBlockingQueue自定义规则排序无限容量初始指定容量,可扩容堆,空间利用高高效,只阻塞take(),不阻塞put()
SynchronousQueue-0-无容量,空间利用高优于其他,不存储,直接传递
DelayQueue延迟排序无限容量优先队列,空间利用高高效,只阻塞take(),不阻塞put()
阻塞和非阻塞队列的并发安全原理是什么?
-并发原理
阻塞队列ReentrantLock以及它的Condition来实现
非阻塞队列利用CAS方法实现
线程工厂

对于线程工厂threadFactory这个性参数,默认提供了defaultThreadFactory。也可以传入自定义的有额外功能的线程工厂,例如为线程设置不同的线程名来区分各个线程池粗行间的线程,方便定为问题。例如

ThreadFactoryBuilder builder = new ThreadFactoryBuilder();
ThreadFactory rpcFactory = builder.setNameFormat("io-pool-%d").build();

其中百分号是指当前线程编号,例如他生成对应的io-pool-1、io-pool-2等线程。

拒绝策略

上面已经介绍过系统提供的四种拒接策略AbortPolicy,DiscardPolicy,DiscardOldestPolicy 或者 CallerRunsPolicy。除了这几个外,我们也可以自定义实现RejectedExecutionHandler接口来实现自己的拒绝策略,在接口中我们要实现rejectedExecution方法用于处理我们想要的逻辑。

private static class CustomRejectionHandler implements RejectedExecutionHandler { 
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 
        //打印日志、暂存任务、重新执行等拒绝策略
    } 
}
小结

定制线程池的各个参数是于业务强相关的,需要根据业务类型来选择各个参数。

shutdown和shutdownNow的区别

首先需要看一下线程池有哪些状态:

RUNNING:接受新任务并处理排队的任务

SHUTDOWN:不接受新任务,但处理排队的任务

STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务

TIDYING:所有任务已终止,workerCount为零,线程转换为TIDYING状态将运行terminated() hook方法

TERMINATED:terminated()已完成,线程池完全关闭

说到关闭首先看一下ThreadPoolExecutor涉及关闭线程池的方法,



public void shutdown() 

public boolean isShutdown()

public boolean isTerminating() 

public boolean awaitTermination(long timeout, TimeUnit unit)

public List<Runnable> shutdownNow()

shutdown()
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

该方法的作用是,将线程池的状态置为SHUTDOWN状态,不接受新提交的任务,队列中的任务会继续执行。调用shutdown() 方法之后并不是立即关闭线程池,因为线程池中可能还有很多任务正在执行,或者在任务队列中有很多任务等待被执行,只有等执行完线程中的任务或者队列中的任务才完全关闭。但是它的作用是,在新提交任务时,线程池会根据拒绝策略直接拒绝掉任务。

isShutdown()

获取当前线程是否关闭,但是这个关闭状态是指的SHUTDOWN以上状态,即返回true,代表着当前状态处于SHUTDOWN、STOP、TIDYING或TERMINATED,所以并不代表着当前线程已经完全关闭,只是开始了关闭流程,此时线程中或者队列中可能还有任务执行。

isTerminating()

当返回true,表示当前线程已经完全关闭,线程中和队列中的任务已经没有了。调用shutdown()方法后,如果线程中任务和队列中的任务还未执行完,isShutdown返回true,但是isTerminating返回false;只有当线程中任务和队列中的任务完全执行完毕,isTerminating才返回true

awaitTermination(long timeout, TimeUnit unit))

它本身并不是用来关闭线程池的,而是主要用来判断线程池状态的。比如我们给 awaitTermination 方法传入的参数是 10 秒,那么它就会陷入 10 秒钟的等待,直到发生以下三种情况之一:

等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true;
等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false;
等待期间线程被中断,方法会抛出 InterruptedException 异常。
也就是说,调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle。

我们则可以根据 awaitTermination() 返回的布尔值来判断下一步应该执行的操作。

shutdownNow()
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

将线程池状态置为STOP,然后想所有工作线程Worker发送中断信号,尝试中断这些任务,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。interruptWorkers()方法会让每一个工作线程中断,这样线程就可以在执行任务期间检测到中断信号并进行相应的处理,提前结束任务。 注 意 点 : j a v a 中 并 不 推 荐 强 行 停 止 线 程 的 机 制 \color{red}{注意点:java中并不推荐强行停止线程的机制} java线,所以即便我们调用了 shutdownNow 方法,如果被中断的线程对于中断信号不理不睬,那么依然有可能导致任务不会停止。这时候就需要我们编写代码时正确来中断线程,响应interrupt型号。

暂时小提一下,在线程中应该是用interrupt()来终止线程,如果有sleep()则需要双重interrupt()来停止;而不是使用volatile标志位来停止,在某些阻塞场景是不适用

复用原理

线程复用原理

线程池是使用固定的线程数或可变的线程数的线程来执行任务,但无论是哪一种,都远远比任务的数量少,因此,线程池通过线程复用让同一个线程执行不同的任务。

线程池将线程和任务解耦,摆脱了每一个任务都需要创建一个线程去执行的限制,大大节约了创建和销毁线程所带来的性能开销。在线程池中,同一个线程可以从BlockingQueue中不断提取新任务来执行,其源码是线程池让每个线程执行一个循环,不停地检查是否还有等待的任务需要被执行,如果有则直接去执行这个任务。

复用源码解析

从执行的代码,execute()去分析。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //工作线程数少于核心线程数量,则添加一个Worker,这里的Worker相当于一个相乘
    if (workerCountOf(c) < corePoolSize) {
        //第二个参数,true->判断是否小于corePoolSize;false->判断是否小于maxPoolSize
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //执行到这里,线程数>=核心线程数
    if (isRunning(c) && workQueue.offer(command)) {
        //如果在运行,且能放入等待队列
        int recheck = ctl.get();
        //如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //线程池是运行状态,线程数为0,则新建线程。用于防止意外终止
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //如果任务未运行,添加Worker,如果失败,则执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

上述代码流程:
1.首先判断线程数是否达到核心线程数,如果没有,则添加核心线程Worker
2.如果线程池在运行中,将任务加入等待队列中。这个时候加入队列,需要检查当前线程池是否还在运行,如果不在则移除任务,执行拒绝策略;如果在运行,则判断是否由于异常情况导致线程数为0,如果异常为0则新建线程
3、如果线程池不在运行状态或等待队列满了,则尝试新建线程,直到达到最大线程数,所以addWorker()的第二个参数传入false,和maxPoolSize判断。如果添加失败,说明线程数已经达到了上线,会根据拒绝策略执行拒绝操作

之后线程启动,来看Worker的run()方法,最终会执行Worker#runWorker()方法,


runWorker(Worker w) {
    Runnable task = w.firstTask;
    while (task != null || (task = getTask()) != null) {
        try {
            task.run();
        } finally {
            task = null;
        }
    }
}

去除队列第一个任务,然后执行任务的run()方法。通过while循环,一直通过 getTask()获取到等待队列中的任务,来执行任务的run()。这里通过线程Worker,循环取不同的任务来执行run()。这样就实现了线程和任务的解耦,线程只管线程运行,任务只是任务,同一个线程可以执行完一个任务后接着取出另一个任务来执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值