【Java多线程】线程池(一)与线程池的初识

文章目录

  • 一.线程的生命周期
    • 1.线程池概念
    • 2.什么场景下适合使用线程池
  • 二.线程池的详解
    • 1.ThreadPoolExecutor参数说明
    • 2.核心线程数(corePoolSize)和最大线程数(maximumPoolSize)
      • 增减线程特点
    • 3.keepAliveTime(空闲时间)
      • 非核心线程存活时间
    • 4.Unit(空闲时间单位)
    • 5.threadFactory(线程工厂)
    • 6.workQueue(工作队列)
    • 7.handler(拒绝策略)
      • RejectedExecutionHandler
    • 8.生成线程规则
      • 线程池里的线程数量设置多少比较合适?
    • 9.停止线程池的正确方法
      • 9.1.shutdown()
      • 9.2.shutdownNow()
      • 9.3.isShutdown(), isTerminated(),awaitTermination()
    • 10.线程池回调函数
    • 11.线程池状态
      • 线程池各个状态切换图
    • 12.线程状态和工作线程数量
  • 三.线程池执行流程
    • 1.提交任务
    • 2.创建工作线程
    • 3.启动线程
    • 4.获取任务并执行
  • 四.线程池的工作队列
    • 1.ArrayBlockingQueue
    • 2.LinkedBlockingQueue
    • 3.PriorityBlockingQueue
    • 4.DelayQueue
    • 5.SynchronousQueue
    • 6.LinkedTransferQueue、LinkedBlockingDeque
    • 7.线程池中有界队列与无界队列区别
        • 7.1.有界队列
        • 7.2.无界队列
        • 7.3.个人理解
  • 五.常用的线程池
    • 1.newFixedThreadPoo
      • 1.1.特点
      • 1.2.工作机制
      • 1.3.结构图
      • 1.4.实例代码
      • 1.5.使用场景
    • 2.newCachedThreadPool
      • 2.1.特点
      • 2.2.工作机制
      • 2.3.结构图
      • 2.4.实例代码
      • 2.5.使用场景
    • 3.newSingleThreadExecutor
      • 3.1.特点
      • 3.2.工作机制
      • 3.3.结构图
      • 3.4.实例代码
      • 3.5.使用场景
    • 4.newScheduledThreadPool
      • 4.1.特点
      • 4.2.工作机制
      • 4.3.实例代码
      • 4.4.使用场景
    • 5.newWorkStealingPool
      • 5.1.特点
      • 5.2.工作机制
      • 5.3.实例代码
      • 5.4.使用场景
    • 6.直接调用JDK封装好的线程池会带来的问题
  • 六.任务提交两种方式
    • 1.execute()和submit()
    • 2.execute()执行流程
      • 2.1.执行流程图
      • 2.2.结构流程图
      • 2.3.举例说明
    • 3.submit和execute区别
      • 3.1.可以接受的任务类型
      • 3.2.返回值
      • 3.3.异常
  • 七.线程池异常处理
    • 1.在run方法中捕获代码可能抛出的所有异常
    • 2.通过Future对象的get方法接收抛出的异常
      • 2.1.了解线程池submit()的执行流程
      • 2.2.举例说明
    • 3.设置UncaughtExceptionHandler (不推荐)
    • 4.重写ThreadPoolExecutor
    • 5. 处理线程池异常的4种方法
  • 八.线程池原理浅析
    • 1.线程池组成部分
    • 2.Executor家族
    • 3.线程池的线程复用的原理
    • 4.使用线程池的注意点
    • 5.线程池特性
      • 5.1.规则描述
      • 5.2.规则验证
        • 5.2.1.验证1
        • 5.2.2.验证2
        • 5.2.3.验证3
        • 5.2.4.验证4
        • 5.2.5.验证5

一.线程的生命周期

【Java多线程】线程池(一)与线程池的初识_第1张图片

  1. new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态
  2. 进入就绪状态的线程开始进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running)
  3. 当该线程的任务执行完成之后或调用的stop()方法之后,线程就进入了死亡状态

线程还具有一个阻塞的过程,当面对以下几种情况的时候,容易造成线程阻塞

  1. 当线程主动调用了sleep()方法时,线程会进入则阻塞状态
  2. 当线程中主动调用了阻塞时的IO方法时,没有获取到IO返回的数据时会一直阻塞
  3. 当线程进入正在等待某个通知时,会进入阻塞状态。

为什么会有阻塞状态出现呢?

  • 我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。

这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?

由以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,

  • 当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,
  • 第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。

1.线程池概念

有时候,系统需要处理非常多的执行时间很短的请求,如果每一个请求都开启一个新线程的话,系统就要不断的进行线程的创建和销毁,有时花在创建和销毁线程上的时间会比线程真正执行的时间还长。而且当线程数量太多时,系统不一定能受得了。

概念:线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。我们将任务添加到任务队列中,线程池得知有任务到来后,会唤醒线程,如若所有线程都在执行任务,则线程会处理完当前任务后,在处理任务队列中的线程。

解决的问题:

  • 重用线程池中的线程,来减少每个线程创建和销毁的性能开销。
  • 对线程进行一些维护和管理,比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。

2.什么场景下适合使用线程池

在实际开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

二.线程池的详解

1.ThreadPoolExecutor参数说明

无论是创建那种类型线程池(FixedThreadPool、CachedThreadPool…),均会调用 ThreadPoolExecutor构造函数

public ThreadPoolExecutor(
		   int corePoolSize, 
		   int maximumPoolSize,
		   long keepAliveTime,
		   TimeUnit unit,
		   BlockingQueue<Runnable> workQueue,
		   ThreadFactory threadFactory,
		   RejectedExecutionHandler handler
		   ) 

参数作用:
【Java多线程】线程池(一)与线程池的初识_第2张图片

  • corePoolSize: 线程池核心线程数最大值,通俗点来讲就是,线程池中常驻线程的最大数量

    • 线程池在创建完时,里面并没有线程,只有当任务到来时再去创建线程。
    • 线程池会一直保持corePoolSize数量的线程,除非设置了 allowCoreThreadTimeOut。
  • maximumPoolSize: 线程池最大线程数大小(非核心线程数,也叫工作线程数,包括核心线程和非核心线程)

  • keepAliveTime: 线程池中空闲线程(即非核心线程)所能存活的最长时间

  • unit: 线程池中空闲线程(即非核心线程存活时间单位,与keepAliveTime配合使用

    如果经过keepAliveTime 时间后,超过核心线程数corePoolSize的线程还没有接受到新的任务,就会被回收

  • workQueue: 存放等待执行任务的阻塞队列

    当提交的任务数超过核心线程数大小corePoolSize后,再提交的任务就存放在这里。仅用来存放被 execute 方法提交的 Runnable 任务。

    • submit方法的底层还是execute方法
  • threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。

  • handler: 线程池的饱和策略事件,主要有4种类型。

    当队列workQueue里面放满了任务、最大线程数maximumPoolSize的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)

2.核心线程数(corePoolSize)和最大线程数(maximumPoolSize)

  • corePoolSize核心线程数,线程池在初始化后,默认情况下不会创建任何线程,会等有任务来的时候才去创建线程核心线程。核心线程会一直存活,即使处于空闲状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true
   /**
     * 线程池线程池创建时是否会初始化所有核心线程
     */
    @Test
    public void testThreadPoolSize() throws InterruptedException {
     
        //创建一个固定长度为50的线程池
        ExecutorService fixedThreadPool = Executors.newScheduledThreadPool(5);

        ThreadPoolExecutor tpe = (ThreadPoolExecutor) fixedThreadPool;
        log.info("线程池初始化=>当前队列任务数={},当前活跃任务数={},执行完成任务数={},总任务数={}", tpe.getQueue().size(), tpe.getActiveCount(), tpe.getCompletedTaskCount(), tpe.getTaskCount());

        for (int i = 0; i < 2; i++) {
     
            tpe.execute(() -> {
     
                System.out.println(Thread.currentThread().getName() + "=>执行任务");
                log.info("任务执行=>当前队列任务数={},当前活跃任务数={},执行完成任务数={},总任务数={}", tpe.getQueue().size(), tpe.getActiveCount(), tpe.getCompletedTaskCount(), tpe.getTaskCount());
            });
        }

        //防止主线程直接结束
        Thread.sleep(5000);

        //根据结果判定:线程池初始化时是不会默认创建多个线程,而是在提交任务时才创建
        log.info("线程池初始化=>当前队列任务数={},当前活跃任务数={},执行完成任务数={},总任务数={}", tpe.getQueue().size(), tpe.getActiveCount(), tpe.getCompletedTaskCount(), tpe.getTaskCount());
    }

执行结果
在这里插入图片描述

  • maximumPoolSize最大线程数。当阻塞队列已满,并且已创建的线程数大于核心线程数且小于最大线程数,则会创建新的线程(非核心线程)去执行任务。因此这个参数只有在阻塞队列满的情况下才有意义对于无界队列,这个参数将会失去效果。

线程总数 = 核心线程数 + 非核心线程数。

增减线程特点

  • corePoolSizemaximumPoolSize设置为相同的值,那么就会创建固定大小的线程池。

  • 如果将线程池的maximumPoolSize参数设置为很大的值,例如Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务。

  • 线程池 只有在队列满了的时候才会去创建大于corePoolSize的线程如果使用了无界队列(如:LinkedBlockingQueue)就不会创建到超过corePoolSize的线程数

3.keepAliveTime(空闲时间)

  • 线程池线程数多于corePoolSize后创建的线程叫非核心线程,非核心线程空闲时间超过keepAliveTime,就会被回收
  • keepAliveTime取值不能小于0,设置为0表示非核心线程线程在空闲时立即终止
  • 如果设置allowCoreThreadTimeOut = true,则keepAliveTime也会作用于核心线程

    注意: keepAliveTime是针对大于核心线程数,小于最大线程数的那部分非核心线程来说的。如果是任务数量特别多的情况下,可以适当增加keepAliveTime的大小。以保证在下个任务到来之前,此线程不会立即销毁,从而避免线程的重新创建。
    .
    一个线程执行完了一个任务后,会去阻塞队列里面取新的任务,在取到任务之前它就是一个闲置的线程。
    .
    tips: 核心线程跟创建的先后没有关系,而是跟工作线程的个数有关,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是“非核心线程”,都有被回收的可能。

    @Test
    public void testThreadPoolKeepAliveTime()  {
     
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1));
        for (int i = 0; i < 3; i++) {
     
            int finalI = i;
            executor.execute(() -> {
     
                System.out.println("i=" + finalI + " Thread = " + Thread.currentThread().getName());
                if (finalI >= 1) {
     
                    try {
     
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                    System.out.println("i=" + finalI + " sleep 1 s结束");
                } else {
     
                    try {
     
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                    System.out.println("i=" + finalI + " sleep 3 s结束");
                }
            });
        }


        while (true) {
     
            System.out.println("总线程数:" + executor.getPoolSize() + "当前活跃线程数:" + executor.getActiveCount());
            try {
     
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
    }

keepAliveTime=0时非核心线程马上就被销毁了
【Java多线程】线程池(一)与线程池的初识_第3张图片
keepAliveTime=10时非核心线程处于空闲时间超过10秒后就被销毁了
【Java多线程】线程池(一)与线程池的初识_第4张图片

非核心线程存活时间

那么什么是“非核心线程”呢?是不是先创建的线程就是核心线程,后创建的就是非核心线程呢?

  • 其实核心线程跟创建的先后没有关系,而是跟工作线程的个数有关如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是“非核心线程”,都有被回收的可能。

Worder线程取任务的方法有两种

  1. 是通过take()一直阻塞直到取出任务
  2. 是通过 poll(keepAliveTime,timeUnit)在一定时间内取出任务或者超时,如果超时这个线程就会被回收(请注意核心线程一般不会被回收。)

那么怎么保证核心线程不会被回收呢?

  • 工作线程的个数有关,每一个线程在取任务的时候,线程池会比较当前的工作线程个数与核心线程数:
    1. 如果 工作线程数小于当前的核心线程数,则使用take()取任务,也就是没有超时回收,这时所有的工作线程都是 “核心线程”,他们不会被回收;

    2. 如果 工作线程数大于当前的核心线程数,则使用 poll(keepAliveTime,timeUnit)取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。

所以每个线程想要保住自己“核心线程”的身份,必须充分努力,尽可能快的获取到任务去执行,这样才能逃避被回收的命运。

核心线程一般不会被回收,但是也不是绝对的,如果我们·设置了允许核心线程超时被回收的话,那么就没有核心线程这种说法了,所有的线程都会通过poll(keepAliveTime, timeUnit)来获取任务,一旦超时获取不到任务,就会被回收,一般很少会这样来使用,除非该线程池需要处理的任务非常少,并且频率也不高,不需要将核心线程一直维持着。

4.Unit(空闲时间单位)

描述存活时间的时间单位。可以使用TimeUnit里边的枚举值。

TimeUnit类中有7种静态属性:

属性 名称
TimeUnit.DAYS
TimeUnit.HOURS 小时
TimeUnit.MINUTES 分钟
TimeUnit.SECONDS
TimeUnit.MILLISECONDS 毫秒
TimeUnit.MICROSECONDS 微妙
TimeUnit.NANOSECONDS

5.threadFactory(线程工厂)

当线程池需要新的线程时,会用threadFactory来生成新的线程。

  • 新的线程由ThreadFactory创建,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程
  • 通常情况下直接使用defaultThreadFactory就行。
  • 如果自己指定ThreadFactory,那么就可以改变线程名线程组优先级是否是守护线程等。
  • ThreadFactory接口,只有一个方法,可以通过实现 ThreadFactory自定义生成线程的规则
    public interface ThreadFactory {
           
    Thread newThread(Runnable r);
    }
    

如默认的Executors.defaultThreadFactory()源码

static class DefaultThreadFactory implements ThreadFactory {
     
  private static final AtomicInteger poolNumber = new AtomicInteger(1);
  private final ThreadGroup group;
  private final AtomicInteger threadNumber = new AtomicInteger(1);
  private final String namePrefix;

  DefaultThreadFactory() {
     
      SecurityManager var1 = System.getSecurityManager();
      this.group = var1 != null?var1.getThreadGroup():Thread.currentThread().getThreadGroup();
      this.namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
  }

  public Thread newThread(Runnable var1) {
     
      Thread var2 = new Thread(this.group, var1, this.namePrefix + this.threadNumber.getAndIncrement(), 0L);
      if(var2.isDaemon()) {
     
          var2.setDaemon(false);
      }

      if(var2.getPriority() != 5) {
     
          var2.setPriority(5);
      }

      return var2;
  }
}

6.workQueue(工作队列)

表示阻塞队列,存储所有等待执行的任务。当核心线程数满了后还有任务继续提交到线程池的话,就先进入workQueue。

  • 直接交接(SynchronousQueue):任务不多时,只需要用队列进行简单的任务中转,这种队列无法存储任务,在使用这种队列时,需要将maximumPoolSize设置的大一点。

  • 无界队列(LinkedBlockingQueue)如果使用无界队列当作workQueue,将maximumPoolSize设置的多大都没有用,不会创建新的非核心线程。使用无界队列的优点是可以防止流量突增缺点是如果处理任务的速度 < 提交任务的速度,会导致无界队列中的任务越来越多,从而导致OOM异常。

无界队列只是相对来说没有限制,大小是int的最大值。

  • 有界队列(ArrayBlockingQueue):使用有界队列可以设置队列大小,让线程池的maximumPoolSize有意义,在有界工作队列已满情况下,继续提交的任务会创建新的线程来执行,但创建的数量不会超过maximumPoolSize,如果超过了maximumPoolSize,就需要采取相应的拒绝策略 RejectedExecutionHandler来应对队列饱和的情况

7.handler(拒绝策略)

表示拒绝策略。当线程池的有界工作队列排满,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务。一共有四种策略可供选择,分别对应四个内部类。

虽然我们有了阻塞队列来对任务进行缓存,这从一定程度上为线程池的执行提供了缓冲期,但是如果是有界的阻塞队列,那就存在队列满的情况,也存在工作线程的数据已经达到最大线程数的时候。如果这时候再有新的任务提交时,显然线程池已经心有余而力不足了,因为既没有空余的队列空间来存放该任务,也无法创建新的线程来执行该任务了,所以这时我们就需要有一种拒绝策略,即 handler。
.
拒绝策略是一个 RejectedExecutionHandler 类型的变量,用户可以自行指定拒绝的策略,如果不指定的话,线程池将使用默认的拒绝策略:抛出异常

拒绝的时机

  • Executor关闭时,新提交的任务会被拒绝(如:执行shutdown()还在继续提交任务)。
  • 当Executor使用有界队列工作队列已满线程池线程数大于最大线程数

拒绝策略分类

策略 描述
AbortPolicy 中断策略,默认策略):直接抛出异常进行拒绝
默认采用的是AbortPolicy,遇到上面两种的情况,线程池直接抛出异常:RejectedExecutionException
DiscardPolicy 丢弃策略):不会得到通知,直接丢弃任务
DiscardOldestPolicy 丢弃最老的):由于队列中存储了很多任务,这个策略会丢弃在队列中存在时间最久的任务。且将当前这个任务继续提交给线程池
CallerRunsPolicy (交给线程池调用所在的线程进行处理,即主线程中执行任务) , 比如主线程给线程池提交任务,但是线程池已经满了,在这种策略下会让提交任务的线程去执行。

个人认为这4中策略不友好,最好自己定义拒绝策略,实现RejectedExecutionHandler接口

RejectedExecutionHandler

可以根据不同场景实现RejectedExecutionHandler接口(接口内只有一个方法),自定义拒绝策略,如记录日志或持久化存储不能处理的任务。

//RejectedExecutionHandler接口
//当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用`RejectedExecutionHandler的rejectedExecution`方法。
public interface RejectedExecutionHandler {
     
  void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);
}

8.生成线程规则

【Java多线程】线程池(一)与线程池的初识_第5张图片

当一个任务被添加进线程池时:

  1. 线程池刚启动的时候 工作线程为0
  2. 线程池中工作线程 < corePoolSize时,即使工作线程处于空闲状态,也会创建一个新线程来执行新任务
  3. 线程池中工作线程 >=corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 。
  4. workQueue已满,且工作线程 < maximumPoolSize时,新提交任务会创建新线程(非核心线程)执行任务。
  5. workQueue已满,且工作线程 > maximumPoolSize时,新提交任务直接采取拒绝策略
  6. 当线程池中的非核心线程 空闲时间达到keepAliveTime`时,将回收这些线程。
  7. 当设置 allowCoreThreadTimeOut(true)时,线程池中核心线程达到keepAliveTime也将被回收。

线程池里的线程数量设置多少比较合适?

类型 描述
CPU密集型(加密、计算hash等) 最佳线程数设置为CPU核心数的1—2倍。
耗时I/O型(读写数据库、文件、网络读写等) 最佳线程数一般会大于CPU核心数很多倍,以JVM监控显示繁忙情况为依据,保证线程空闲可以衔接上。
参考Brain Goezt推荐的计算方法:线程数=CPU核心数 × (1+平均等待时间/平均工作时间)

9.停止线程池的正确方法

9.1.shutdown()

shutdown(): 调用了shutdown()方法不一定会立即停止,会拒绝接受新的任务,同时等现有任务执行完后才退出线程池

  • 因为线程池中的线程有可能正在运行,并且队列中也有待处理的任务不可能说停就停。所以每当调用该方法时,线程池会把正在执行的任务队列中等待的任务执行完毕再关闭,并且在此期间如果接收到新的任务会被拒绝
 public static void main(String[] args) throws InterruptedException {
     
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
     
            //使用lambda的方式提交任务
            int finalI = i;
            executorService.execute( () -> {
     
                try {
     
                    Thread.sleep(500);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                System.out.println(finalI +"=>"+Thread.currentThread().getName());
            });
        }
		
		//主线程休眠1.5s后在关闭线程池
        Thread.sleep(1500);

        //关闭线程池
        executorService.shutdown();

        //关闭线程池再次提交任务,线程池会把正在执行的任务和队列中等待的任务都执行完毕再关闭,关闭后接收到新的任务会被拒绝,抛出java.util.concurrent.RejectedExecutionException
        executorService.execute( () -> {
     
            System.out.println("再次提交任务=>"+Thread.currentThread().getName());
        });
    }

在这里插入图片描述
结论: 关闭线程池后,线程池会把正在执行的任务和队列中等待的任务都执行完毕再关闭,而关闭后接收到新的任务会被拒绝,并抛出java.util.concurrent.RejectedExecutionException异常

9.2.shutdownNow()

shutdownNow(): 调用了这个方法时,线程池会立即终止,并返回没有被处理完的任务。如果需要继续执行这里的任务可以再次让线程池执行这些返回的任务。

尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。从此方法返回时,将从任务队列中删除这些任务。 执行当前后不管现在线程池的运行状况,直接一刀切全部停掉,这样可能会导致任务丢失

    public static void main(String[] args) throws InterruptedException {
     
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
     
            //使用lambda的方式提交任务
            int finalI = i;
            executorService.execute(() -> {
     
                try {
     
                    Thread.sleep(500);//当线程在活动之前或活动期间处于正在等待、休眠或占用状态且该线程被中断时,抛出该异常。
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                System.out.println(finalI + "=>" + Thread.currentThread().getName());
            });
        }

        Thread.sleep(1500);
        System.out.println("isShutDown1=>" + executorService.isShutdown());

        //关闭线程池,并返回没有被处理完的任务
        //该方法使得主线程强行打断子线程的sleep状态,因此抛出此异常:java.lang.InterruptedException: sleep interrupted,根据实际情况,shutdownNow()这个不合理的方法,可以解决该异常。
        List<Runnable> unfinishedTask = executorService.shutdownNow();
        for (Runnable runnable : unfinishedTask) {
     
            new Thread(runnable,"重新执行").start();
        }

        System.out.println("unfinishedTask=>"+unfinishedTask.size());

        System.out.println("isShutDown2=>" + executorService.isShutdown());
    }

【Java多线程】线程池(一)与线程池的初识_第6张图片
【Java多线程】线程池(一)与线程池的初识_第7张图片

9.3.isShutdown(), isTerminated(),awaitTermination()

isShutdown():可以用于判断线程池是否被shutdown了,是true,否则false
isTerminated():可以判断线程是否被完全终止了,如果线程池关闭后所有任务都已完成返回true,否则返回false
awaitTermination():传入等待时间,等待时间达到时判断是否停止了,主要用于检测。

    public static void main(String[] args) throws InterruptedException {
     
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
     
            //使用lambda的方式提交任务
            int finalI = i;
            executorService.execute( () -> {
     
                try {
     
                    Thread.sleep(500);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                System.out.println(finalI +"=>"+Thread.currentThread().getName());
            });
        }

        //主线程休眠1.5s后在关闭线程池
        Thread.sleep(1500);

        System.out.println("isShutDown1=>"+executorService.isShutdown());

        //关闭线程池
        executorService.shutdown();

        System.out.println("isShutDown2=>"+executorService.isShutdown());
        System.out.println("isTerminated=>"+executorService.isTerminated());
    }

在这里插入图片描述

上图关闭线程池后isTerminated=false,说明有正在执行的任务,以及队列中正在等待执行的任务, 导致线程池没有立刻关闭.

在代码中调用主线程休眠10s后在关闭线程池,保证线程池所有正在运行的任务,以及队列中的任务执行完毕
【Java多线程】线程池(一)与线程池的初识_第8张图片
【Java多线程】线程池(一)与线程池的初识_第9张图片

10.线程池回调函数

线程池ThreadPoolExecutor为了提供扩展,提供了protected的两个方法beforeExecute和 afterExecute每个任务执行前后都会调用这两个方法,相当于对线程任务的执行做了一个切面

{
     
    static class Customer extends Thread{
     
        private String name;
        public Customer(String name){
     
            this.name = name;
        }
        @Override
        public void run() {
     
            System.out.println(this.name+":加入线程池");
            try {
     
                Thread.sleep(ThreadLocalRandom.current().nextInt(10000)*6);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
     
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,4,5,
                TimeUnit.SECONDS,new ArrayBlockingQueue<>(9),new ThreadPoolExecutor.DiscardOldestPolicy()){
     
            /**
             *
             * @param t   执行任务的线程
             * @param r 将要被执行的任务
             */
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
     
                System.out.println(((Customer)r).name + ":beforeExecute将要被执行");
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
     
                System.out.println(((Customer)r).name + ":afterExecute已经执行完毕");

            }
        };
        for (int i = 0; i < 10; i++) {
     
            poolExecutor.execute(new Customer("customer-"+i));
        }
        poolExecutor.shutdown();
    }
}

【Java多线程】线程池(一)与线程池的初识_第10张图片

/**
 * 演示每个任务执行的前后放钩子函数
 */
public class PauseableThreadPool extends ThreadPoolExecutor {
     

    private final ReentrantLock lock = new ReentrantLock();
    private boolean isPaused;
    private Condition unPaused = lock.newCondition();

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                               TimeUnit unit, BlockingQueue<Runnable> workQueue) {
     
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }


    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                               TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
     
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }


    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                               TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
     
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                               TimeUnit unit, BlockingQueue<Runnable> workQueue,
                               ThreadFactory threadFactory, RejectedExecutionHandler handler) {
     
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    public static void main(String[] args) throws InterruptedException {
     
        PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10L,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        Runnable runnable = new Runnable() {
     
            @Override
            public void run() {
     
                try {
     
                    Thread.sleep(50);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 100; i++) {
     
            pauseableThreadPool.execute(runnable);
        }
        Thread.sleep(1500);
        pauseableThreadPool.pause();
        System.out.println("线程池被暂停了");
        Thread.sleep(1500);
        pauseableThreadPool.resume();
        System.out.println("线程池被恢复了");

        Thread.sleep(1500);
        System.out.println("关闭线程池");
        pauseableThreadPool.shutdown();
    }

    /**
     * 线程执行之前
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
     
        super.beforeExecute(t, r);
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"=>beforeExecute");
        try {
     
            while (isPaused) {
     
                unPaused.await();
            }
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            lock.unlock();
        }
    }

    /**
     * 线程执行之后
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
     
        System.out.println(Thread.currentThread().getName()+"=>afterExecute");
        super.afterExecute(r, t);
    }

    /**
     * 线程暂停
     */
    private void pause() {
     
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"=>pause");
        try {
     
            isPaused = true;
        } finally {
     
            lock.unlock();
        }
    }

    /**
     * 线程被唤醒
     */
    public void resume() {
     
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"=>resume");
        try {
     
            isPaused = false;
            //唤醒全部
            unPaused.signalAll();
        } finally {
     
            lock.unlock();
        }
    }

    /**
     * 线程池关闭后
     */
    @Override
    protected void terminated() {
     
        System.out.println(Thread.currentThread().getName()+"=>terminated");
        super.terminated();
    }
}
 /**     
     * @param t 执行任务的线程
     * @param r 将要被执行的任务
     */
    protected void beforeExecute(Thread t, Runnable r) {
      }
 
    /**
     * @param r 将要被执行的任务
     * @param t 异常信息
     */
    protected void afterExecute(Runnable r, Throwable t) {
      }

【Java多线程】线程池(一)与线程池的初识_第11张图片

11.线程池状态

线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。 对应java.util.concurrent.ThreadPoolExectutor的5种状态

private static final int RUNNING    = -1 << COUNT_BITS;//运行
private static final int SHUTDOWN   =  0 << COUNT_BITS;//关闭
private static final int STOP       =  1 << COUNT_BITS;//停止
private static final int TIDYING    =  2 << COUNT_BITS;//
private static final int TERMINATED =  3 << COUNT_BITS;//终止

线程池各个状态切换图

【Java多线程】线程池(一)与线程池的初识_第12张图片

RUNNING(运行状态)

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

    接受新任务并处理排队任务

SHUTDOWN(待关闭状态)

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

    当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态
    不接受新的任务但是处理排队任务

STOP(停止状态)

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务,
  • 线程池中执行的任务为空,进入TIDYING状态;

    当工作线程数为0时,进入 TIDYING 状态
    就是调用shutdownNow()带来的效果

TIDYING(整理状态)

  • 该状态表明所有的任务已经运行终止记录的任务数量为0
  • terminated()执行完毕,进入TERMINATED状态

    任务都已经终止,workerCount0时,线程会转换到TIDYING状态,并将运行terminate()钩子方法

TERMINATED(终止状态)

  • 该状态表示线程池彻底终止,并完成了所有资源的释放

    terminate()运行完成

12.线程状态和工作线程数量

线程池是有状态的,不同状态下线程池的行为是不一样的,而且控制线程资源合理高效的使用,必须控制工作线程的个数,所以线程池内部需要保存当前线程池中工作线程的个数与状态

看到这里,你是线程池内部是用两个变量来保存线程池的状态和线程池中工作线程的个数呢?

  • ThreadPoolExecutor中只用了一个AtomicInteger 型的变量就保存了这两个属性的值,那就是 ctl
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    
  • ctl高3位用来表示线程池的状态(runState)低29位用来表示工作线程的个数(workerCnt)

【Java多线程】线程池(一)与线程池的初识_第13张图片
为什么要用3位来表示线程池的状态呢?
原因是线程池一共有5种状态,而2位只能表示出4种情况,所以至少需要3位才能表示得了5种状态。

三.线程池执行流程

【Java多线程】线程池(一)与线程池的初识_第14张图片
上图是一张线程池工作的精简图,实际的过程比这个要复杂的多,不过这些应该能够完全覆盖到线程池的整个工作流程了。
整个过程可以拆分成以下几个部分:

1.提交任务

当向线程池提交一个新的任务时,线程池有三种处理情况,分别是:1.创建一个工作线程来执行该任务、2.将任务加入阻塞队列、3.拒绝该任务。

提交任务的过程也可以拆分成以下几个部分:

  • 当工作线程数小于核心线程数时,直接创建新的核心工作线程
  • 当工作线程数不小于核心线程数时,就需要尝试将任务添加到阻塞队列中去
  • 如果能够加入成功,说明队列还没有满,那么需要做以下的二次验证来保证添加进去的任务能够成功被执行
    • 验证当前线程池的运行状态,如果是非RUNNING状态,则需要将任务从阻塞队列中移除,然后拒绝该任务
    • 验证当前线程池中的工作线程的个数,如果为0,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务
  • 如果加入失败,则说明队列已经满了,那么这时就需要创建新的“临时”工作线程来执行任务
  • 如果创建成功,则直接执行该任务
  • 如果创建失败,则说明工作线程数已经等于最大线程数了,则只能拒绝该任务了

整个过程可以用下面这张图来表示:
【Java多线程】线程池(一)与线程池的初识_第15张图片

2.创建工作线程

创建工作线程需要做一系列的判断,需要确保当前线程池可以创建新的线程之后,才能创建。

  1. 首先,当线程池的状态是SHUTDOWN 或者 STOP 时,则不能创建新的线程。
  2. 线程工厂创建线程失败时,也不能创建新的线程。
  3. 当前工作线程的数量与核心线程数、最大线程数进行比较,如果前者大于后者的话,也不允许创建。
  4. 除此之外,会尝试通过CAS 来自增工作线程的个数,如果自增成功了,则会创建新的工作线程,即 Worker 对象。然后加锁进行二次验证是否能够创建工作线程,最后如果创建成功,则会启动该工作线程。

3.启动线程

  1. 工作线程创建成功后,也就是 Worker 对象已经创建好了,这时就需要启动该工作线程,让线程开始干活了,Worker 对象中关联着一个 Thread,所以要启动工作线程的话,只要通过 worker.thread.start()来启动该线程即可。
  2. 启动完了之后,就会执行 Worker对象的 run 方法,因为 Worker 实现了 Runnable 接口,所以本质上 Worker 也是一个线程。
  3. 通过线程 start()开启之后就会调用到 Runnable 的run 方法,在 worker 对象的 run 方法中,调用了 runWorker(this)方法,也就是把当前对象传递给了 runWorker方法,让他来执行。

4.获取任务并执行

  1. runWorker 方法被调用之后,就是执行具体的任务了,首先需要拿到一个可以执行的任务,而 Worker 对象中默认绑定了一个任务,如果该任务不为空的话,那么就是直接执行

  2. 执行完了之后,就会去阻塞队列中获取任务来执行,而获取任务的过程,需要考虑当前工作线程的个数

    • 如果工作线程数大于核心线程数,那么就需要通过poll来获取,因为这时需要对闲置的线程进行回收;
    • 如果工作线程数小于等于核心线程数,那么就可以通过take来获取了,因此这时所有的线程都是核心线程,不需要进行回收,前提是没有设置 allowCoreThreadTimeOut

四.线程池的工作队列

1.ArrayBlockingQueue

ArrayBlockingQueue: (有界队列)是一个用数组实现的有界阻塞队列,特性先进先出(FIFO)。 支持公平锁与非公平锁

    @Test
    public void testArrayBlockingQueue() throws InterruptedException {
     
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(5);

        //生产者(添加元素)
        new Thread( () -> {
     
            while (true) {
     
                try {
     
                    String data = UUID.randomUUID().toString();
                    queue.put(data);
                    System.out.println("Put: " + data+"——size:"+queue.size());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }).start();

        //消费者1(取出元素)
        new Thread( () -> {
     
            while (true) {
     
                try {
     
                    String data = queue.take();
                    System.out.println(Thread.currentThread().getName() + " take(): " + data+"——size:"+queue.size());
                    Thread.sleep(1200);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }).start();


        //休眠100S防止主线程直接结束
        Thread.sleep(100000);
    }

执行结果
【Java多线程】线程池(一)与线程池的初识_第16张图片

2.LinkedBlockingQueue

LinkedBlockingQueue: (可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择性进行设置,不设置的话,将是一个无界阻塞队列,最大长度和默认长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene

newFixedThreadPool线程池使用了这个队列

@Test
    public void testLinkedBlockingQueue() throws InterruptedException {
     
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue(5);

        //生产者(添加元素)
        new Thread( () -> {
     
            while (true) {
     
                try {
     
                    String data = UUID.randomUUID().toString();
                    queue.put(data);
                    System.out.println("Put: " + data+"——size:"+queue.size());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }).start();

        //消费者1(取出元素)
        new Thread( () -> {
     
            while (true) {
     
                try {
     
                    String data = queue.take();
                    System.out.println(Thread.currentThread().getName() + " take(): " + data+"——size:"+queue.size());
                    Thread.sleep(1200);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }).start();


        //休眠100S防止主线程直接结束
        Thread.sleep(100000);
    }

执行效果:
【Java多线程】线程池(一)与线程池的初识_第17张图片

3.PriorityBlockingQueue

PriorityBlockingQueue:(优先级队列):使用平衡二叉树堆实现的具有优先级无界阻塞队列传入的对象必须实现Comparable接口,也可以构造方法传入比较器Comparator。默认按照自然顺序排序

    @Test
    public void testPriorityBlockingQueue() throws InterruptedException {
     
        //使用默认排序方式,即按自然顺序排序(即从小到大),可以通过元素实现Comparable接口或者构造时传入Comparator进行自定义排序
        PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue(5);
        queue.put(6);
        queue.put(4);
        queue.put(3);
        queue.put(1);
        queue.put(2);
        queue.put(7);

        System.out.println(queue.poll());//1
        System.out.println(queue.poll());//2
    }

执行结果
【Java多线程】线程池(一)与线程池的初识_第18张图片

4.DelayQueue

DelayQueue: (延迟队列)是一个内部通过PriorityBlockingQueue(根据时间大小排序)实现的延时获取无界阻塞队列。创建元素的必须实现Delay接口,指定从队列中获取当前元素的时间。

newScheduledThreadPool线程池使用了这个队列。

public interface Delayed extends Comparable<Delayed> {
     
    long getDelay(TimeUnit unit);
}

队列中每个元素均有过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最块要过期的元素。
过期元素才会出队列。队列头元素是最块要过期的元素。

具体实例

//DelayQueue保存的元素
public class Item implements Delayed {
     
    String name;

    //触发时间
    private long time;

    public Item(String name, long time, TimeUnit unit) {
     
        this.name = name;
        this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
    }

    @Override
    public long getDelay(TimeUnit unit) {
     
        return time - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
     
        Item item = (Item) o;
        long diff = this.time - item.time;
        if (diff <= 0) {
     // 改成>=会造成问题
            return -1;
        } else {
     
            return 1;
        }
    }

    @Override
    public String toString() {
     
        return "Item{" +
                "time=" + time +
                ", name='" + name + '\'' +
                '}';
    }

    @Test
    public void testDelayQueue() throws InterruptedException {
     
        Item item1 = new Item("item1", 5, TimeUnit.SECONDS);
        Item item2 = new Item("item2", 10, TimeUnit.SECONDS);
        Item item3 = new Item("item3", 15, TimeUnit.SECONDS);

        DelayQueue<Item> queue = new DelayQueue<>();
        queue.put(item1);
        queue.put(item2);
        queue.put(item3);

        System.out.println("begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        for (int i = 0; i < 3; i++) {
     
            Item take = queue.take();
            System.out.format("name:{%s}, time:{%s}\n", take.name, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
        }
        /*
         * begin time:2020-07-07T17:19:53.038
         * name:{item1}, time:{2020-07-07T17:19:57.982}
         * name:{item2}, time:{2020-07-07T17:20:02.982}
         * name:{item3}, time:{2020-07-07T17:20:07.982}
         */
    }
}

5.SynchronousQueue

SynchronousQueue:(同步队列)一个不存储元素的阻塞队列,每个插入操作(put)必须等到另一个线程调用移除操作(take),否则 插入操作一直处于阻塞状态,支持公平锁与非公平锁,吞吐量通常要高于LinkedBlockingQuene

newCachedThreadPool线程池使用了这个队列。新任务到了如果有空闲线程则使用空闲线程执行,没有就创建新线程,不会对任务进行缓存

    @Test
    public void testSynchronousQueue() throws InterruptedException {
     
        SynchronousQueue<String> queue = new SynchronousQueue();

        //生产者(添加元素)
        new Thread( () -> {
     
            while (true) {
     
                try {
     
                    String data = UUID.randomUUID().toString();
                    System.out.println("Put: " + data);
                    queue.put(data);
                    Thread.sleep(100);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }).start();

        //消费者1(取出元素)
        new Thread( () -> {
     
            while (true) {
     
                try {
     
                    String data = queue.take();
                    System.out.println(Thread.currentThread().getName()
                            + " take(): " + data);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }).start();

        Thread.sleep(100000);
    }

执行结果
【Java多线程】线程池(一)与线程池的初识_第19张图片

6.LinkedTransferQueue、LinkedBlockingDeque

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,相当于其它队列多了transfer和tryTransfer方法

  • 若当前存在一个正在等待获取元素take()的消费者线程,就直接将元素 “交给” 等待者;否则,会插入当前元素到队列尾部,并且等待进入阻塞状态,等待消费者线程取走该元素。
  • put和 transfer 方法的区别是,put 是立即返回的, transfer 是阻塞等待消费者拿到数据才返回。

思想: LinkedTransferQueue采用一种预占模式。意思就是消费者线程取(take)元素时,如果队列不为空,则直接取走(take)数据,若队列为空,那就生成一个节点(节点元素为null)入队(put),然后消费者线程被等待在这个节点上,后面生产者线程入队时(put)发现有一个元素为null的节点,生产者线程就不入队了(no put),直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素(take),从调用的方法返回。我们称这种节点操作为“匹配”方式
【Java多线程】线程池(一)与线程池的初识_第20张图片
并发编程—— LinkedTransferQueue

LinkedBlockingDeque: 是一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加(put)和移除(put)元素 ,因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争

  • 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。

7.线程池中有界队列与无界队列区别

有界队列: 就是有设置固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue、又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue

无界队列: 指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于“无界”。比如没有设定固定大小的 LinkedBlockingQueue。

7.1.有界队列

  1. 当工作线程数
  2. 当工作线程数>corePoolSize,会将提交的任务到一个阻塞队列中中,。
  3. 有界队列满了之后,如果当前总线程数 < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。
  4. 如果3中也无法处理了,就会走到第四步执行reject操作(拒绝提交)。

7.2.无界队列

  1. 与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况。
  2. 当有新的任务到来,当前线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续新建线程了,若后面还有新的任务提交,而且没有空闲的线程资源,则任务直接进入队列等待。
  3. 若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。

对于无界队列来讲,当核心线程数满了后,任务优先进入等待队列。如果等待队列也满了后,才会去创建新的非核心线程 。
所以即使线程池的maximumPoolSize的设置的再大对于线程的执行是没有影响的。

7.3.个人理解

队列为有界无界,需要看创建创建队列时有没有指定容量有设置大小的为有界队列,没有设置大小的为无界队列(Integer.MAX_VALUE)

(如果队列是无界队列,任务来了可以直接入队,几乎不会满,除非系统资源耗尽了)

五.常用的线程池

1.newFixedThreadPoo

固定数量线程的线程池

  • 构造方法
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
     
 return new ThreadPoolExecutor(
 			nThreads, 
 			nThreads,
 			0L, 
 			TimeUnit.MILLISECONDS,
 			new LinkedBlockingQueue<Runnable>(),
 			threadFactory
 		);
}

1.1.特点

  • 核心线程数和最大线程数大小一样 (没有非核心线程
  • 没有所谓的非空闲时间,即 keepAliveTime=0 (内部的核心线程全部不销毁)
  • 阻塞队列为无界队列LinkedBlockingQueue
  • 核心线程数等于最大线程数,所以线程池中只有核心线程, 除非线程池被关闭,否则核心线程线程不会被回收
  • 当所有的线程都处于活动状态时,新的任务都会处于等待状态,直到有线程空闲出来。
  • 阻塞队列是无界队列,不会执行拒绝策略,可能会在任务队列中堆集无限的请求,导致OOM

1.2.工作机制

【Java多线程】线程池(一)与线程池的初识_第21张图片

  • 提交任务
  • 如果线程数 < 核心线程,创建核心线程执行任务
  • 如果线程数 = 核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

1.3.结构图

【Java多线程】线程池(一)与线程池的初识_第22张图片

  • 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。

  • 在线程数目达到corePoolSize后,将新任务放到LinkedBlockingQueue阻塞队列中。

  • 线程执行完(1)中任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。

1.4.实例代码

ExecutorService executor = Executors.newFixedThreadPool(10);
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
     
        executor.execute(()->{
     
          try {
     
              Thread.sleep(10000);
              } catch (InterruptedException e) {
     
                     //do nothing
          }
});

IDEA 指定JVM参数:-Xmx8m -Xms8m
【Java多线程】线程池(一)与线程池的初识_第23张图片run以上代码,会抛出OOM:
【Java多线程】线程池(一)与线程池的初识_第24张图片
面试题:使用无界队列的线程池会导致内存飙升吗?
答案 :会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。

1.5.使用场景

  • FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

2.newCachedThreadPool

可缓存,无界的,自动回收多余线程线程的线程池。

它没有需要维护的核心线程数,每当需要线程的时候就进行创建,因为它的线程存活时间是60秒,所以它也凭借着这个参数实现了自动回收的功能。

  • 构造方法
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
     
  return new ThreadPoolExecutor(
  		0, 
  		Integer.MAX_VALUE,
    	60L, 
    	TimeUnit.SECONDS,
    	new SynchronousQueue<Runnable>(),
    	threadFactory
    );
}

2.1.特点

  • 核心线程数为0(没有核心线程,全部是非核心线程)
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • keepAliveTime=60 非核心线程空闲存活时间为60秒

核心线程数为0,总线程数量阈值为Integer.MAX_VALUE,即可以创建无限的非核心线程

  • 当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
  • 采用SynchronousQueue,每当提交一个任务,都会超过阻塞队列的长度,导致创建线程,所以说:每当提交一个任务,都会创建一个线程,可能造成OOM。
  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程,超过60s则销毁线程(非核心线程),一定程度减少频繁创建/销毁线程的系统开销

2.2.工作机制

【Java多线程】线程池(一)与线程池的初识_第25张图片

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

2.3.结构图

【Java多线程】线程池(一)与线程池的初识_第26张图片

2.4.实例代码

  ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
     
            executor.execute(() -> {
     
                System.out.println(Thread.currentThread().getName()+"正在执行");
            });
        }

运行结果:
【Java多线程】线程池(一)与线程池的初识_第27张图片

2.5.使用场景

  • 用于并发执行大量短期的小任务。因为最大线程数为Integer.MAX_VALUE,所以提交任务的速度 > 线程池中线程处理任务的速度 就会不断创建新线程;每次提交任务,都会立即有线程去处理,因此CachedThreadPool适用于处理大量耗时少的任务

3.newSingleThreadExecutor

单线程的线程池,全程只以1条线程执行任务

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

3.1.特点

  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是无界队列 LinkedBlockingQueue
  • keepAliveTime为0
  • 所有任务按照指定顺序执行,即遵循队列的入队出队规则
  • 缺点和固定线程池一样,可能会在任务队列中堆集无限的请求,导致OOM

和一个线程的区别

newSingleThreadExecutor Thread
任务执行完成后,不会自动销毁,可以复用 任务执行完成后,会自动销毁
可以将任务存储在阻塞队列中,逐个执行 无法存储任务,只能执行一个任务

3.2.工作机制

【Java多线程】线程池(一)与线程池的初识_第28张图片

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,讲任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条- 线程)夜以继日地干活。

3.3.结构图

【Java多线程】线程池(一)与线程池的初识_第29张图片

  • 当线程池中没有线程时,会创建一个新线程来执行任务。
  • 当前线程池中有一个线程后,将新任务加入LinkedBlockingQueue
  • 线程执行完第一个任务后,会在一个无限循环中反复从LinkedBlockingQueue 获取任务来执行。

3.4.实例代码

ExecutorService executor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 5; i++) {
     
         executor.execute(() -> {
     
   				System.out.println(Thread.currentThread().getName()+"正在执行");
         });
}

运行结果:
【Java多线程】线程池(一)与线程池的初识_第30张图片

3.5.使用场景

  • 适用于串行执行任务的场景,一个任务一个任务地执行。

4.newScheduledThreadPool

定时及周期执行的线程池

  • 构造方法
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
     
  return new ThreadPoolExecutor(
  		0, 
  		Integer.MAX_VALUE,
  		60L, TimeUnit.SECONDS,
  		new SynchronousQueue<Runnable>(),
  		threadFactory
  );
}

4.1.特点

  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate :按照固定速率周期执行
  • scheduleWithFixedDelay:上个任务延迟固定时间后执行

4.2.工作机制

  • 添加一个任务
  • 线程池中的线程从 DelayQueue 中取任务
  • 线程从 DelayQueue 中获取 time 大于等于当前时间的task
  • 执行完后修改这个 task 的 time 为下次被执行的时间
  • 这个 task 放回DelayQueue队列中

4.3.实例代码

scheduleAtFixedRate()方法

 @Test
    public void scheduleAtFixedRate() throws InterruptedException {
     
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

        // 1秒后开始执行定时任务,每3秒执行一次:
        scheduledExecutorService.scheduleAtFixedRate(() -> {
     
            try {
     
                Thread.sleep(2000);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println("current Time" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "正在执行");
        }, 1, 3, TimeUnit.SECONDS);
        ;

        //防止主线程结束
        Thread.sleep(100000);

        //关闭线程池
        scheduledExecutorService.shutdown();
    }

执行结果
【Java多线程】线程池(一)与线程池的初识_第31张图片
结论:scheduleAtFixedRate(commod,initialDelay,period,unit),这个是以period为固定周期时间,按照一定频率来重复执行任务,initialDelay说系统启动后,需要等待多久才开始执行。例如:如果设置了period为5秒,线程启动之后执行了大于5秒,线程结束之后,立即启动线程的下一次,如果线程启动之后只执行了3秒就结束了那执行下一次,需要等待2秒再执行。这个是优先保证任务执行的频率

scheduleWithFixedDelay()方法

    @Test
    public void scheduleWithFixedDelay() throws InterruptedException {
     
        /*
         * 创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间  + 给定的间隔时间
         */
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

        long startTime = System.currentTimeMillis();
        //5秒后开始执行定时任务,以3秒为间隔执行:
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
     
            System.out.println("current Time=>" +  System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "正在执行");
        }, 5, 3, TimeUnit.SECONDS);


        //防止主线程结束
        Thread.sleep(100000);

        //关闭线程池
        scheduledExecutorService.shutdown();
    }

执行结果
【Java多线程】线程池(一)与线程池的初识_第32张图片
结论:scheduleWithFixedDelay(commod,initialDelay,delay,unit),这个是以delay为固定延迟时间,按照一定的等待时间来执行任务,initialDelay也是说系统启动后,需要等待多久才开始执行。例如:设置了delay为5秒,线程启动之后不管执行了多久,结束之后都需要等待5秒,才能执行下一次。这个是优先保证任务执行的间隔。

schedule(commod,delay,unit)
这个方法是说系统启动后,需要等待多久执行,delay是等待时间。只执行一次,没有周期性。

4.4.使用场景

周期性执行任务的场景,需要限制线程数量的场景

回到面试题:说说几种常见的线程池及使用场景?

回答这四种经典线程池 :newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool,newScheduledThreadPool,分线程池特点,工作机制,使用场景分开描述,再分析可能存在的问题,比如newFixedThreadPool内存飙升问题 即可

除了newScheduledThreadPool的内部实现特殊一点之外,其它几个线程池都是基于ThreadPoolExecutor类实现的。

5.newWorkStealingPool

5.1.特点

JDK1.8新增的线程池,是基于ForkJoinPool 的扩展.的工作窃取线程池,每个线程都有一个任务队列存放任务,当前线程的任务队列为空时,会根据工作窃取算法去其他任务的工作队列 中窃取任务执行

  • 最大线程数为Runtime.getRuntime().availableProcessors(),即cpu逻辑核心数
  • WorkStealingPool 底层是使用 ForkJoinPool实现的
  • 采用工作窃取算法每个工作线程分配一个双端队列(本地队列)用于存放需要执行的任务,当自己的任务队列没有数据的时候随机从其它工作线程的任务队列中获得一个任务继续执行。

5.2.工作机制

【Java多线程】线程池(一)与线程池的初识_第33张图片
如果工作线程先把自己队列里的任务处理完了,而其他工作线程对应的队列里还有任务等待处理。处理完任务的工作线程就去其他工作线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,这样就避免空闲工作线程因为没有任务执行而产生无意义的等待。所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列被窃取任务线程 永远从双端队列的 "头部" 拿任务执行,而窃取任务的线程永远从双端队列的 "尾部" 拿任务执行

5.3.实例代码

假设共有三个线程同时执行, A, B, C

  • 当A,B线程尚未处理任务结束,而C已经处理完毕,则C线程会从A或者B中窃取任务执行,这就叫工作窃取
  • 假如A线程中的队列里面分配了5个任务,而B线程的队列中分配了1个任务,当B线程执行完任务后,它会主动的去A线程中窃取其他的任务进行执行
public class WorkStealingPool {
     

    public static void main(String[] args) throws IOException {
     //
        // CPU 核数
        System.out.println(Runtime.getRuntime().availableProcessors());

        // workStealingPool 会自动启动cpu核数个线程去执行任务
        ExecutorService service = Executors.newWorkStealingPool();
        service.execute(new MyRunnable(10000));  // 我的cpu核数为12 启动13个线程,其中第一个是1s执行完毕,其余都是2s执行完毕,
        // 有一个任务会进行等待,当第一个执行完毕后,会再次偷取第十三个任务执行
        for (int i = 0; i < Runtime.getRuntime().availableProcessors(); i++) {
     
            service.execute(new MyRunnable(2000));
        }

        // 因为work stealing 是deamon线程,即后台线程,精灵线程,守护线程
        // 所以当main方法结束时, 此方法虽然还在后台运行,但是无输出
        // 可以通过对主线程阻塞解决
        System.in.read();
    }

    static class MyRunnable implements Runnable {
     

        int time;

        MyRunnable(int time) {
     
            this.time = time;
        }

        @Override
        public void run() {
     
            try {
     
                TimeUnit.MILLISECONDS.sleep(time);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "  " + time);
        }
    }
}

执行结果
【Java多线程】线程池(一)与线程池的初识_第34张图片

5.4.使用场景

底层是通过ForkJoinPool实现的,最适合计算密集型(CPU密集型)的任务

  • 通过将一个大任务分割为若干互不依赖的子任务,为了减少线程间的竞争,再把这些子任务分别放到不同的工作队列里,并为每个工作队列创建一个单独的工作线程来执行队列里的任务,工作线程和队列一一对应,比如A线程负责处理A队列里的任务。如果工作线程先把自己队列里的任务处理完了,而其他工作线程对应的队列里还有任务等待处理处理完任务的工作线程就去其他工作线程的队列里窃取一个任务来执行。
    • 充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。且该算法消耗了更多的系统资源,比如创建多个工作线程以及对应多个双端队列。

6.直接调用JDK封装好的线程池会带来的问题

使用无界队列的线程池会导致内存飙升吗?
答案 :会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。

六.任务提交两种方式

1.execute()和submit()

线程池框架提供了两种方式提交任务,根据不同的业务需求选择不同的方式。

  1. Executor.execute()
    通过Executor.execute()方法提交的任务,必须实现Runnable接口,该方式提交的任务不能获取返回值,因此无法判断任务是否执行成功。

  2. ExecutorService.submit()
    通过ExecutorService.submit()方法提交的任务,可以获取任务执行完的返回值。

    ctrl加鼠标左键 进入submit,查看AbstractExecutorService,发现submit底层调用的还是execute,但是提交的任务不是task而是在task的基础上封装了一层FutureTask

    public Future<?> submit(Runnable task) {
           
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
    }
    

2.execute()执行流程

2.1.执行流程图

线程池执行流程,即对应execute()方法:

【Java多线程】线程池(一)与线程池的初识_第35张图片

  • 提交一个任务,线程池里存活的核心线程数小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
  • 如果线程池核心线程数已满即线程数已经等于corePoolSize,一个新提交的任务,会被放进 任务队列workQueue 排队等待执行
  • 当线程池里面存活的线程数已经 等于 corePoolSize了,并且任务队列workQueue也满了,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务
  • 如果当前的线程数达到maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理

2.2.结构流程图

【Java多线程】线程池(一)与线程池的初识_第36张图片

2.3.举例说明

为了形象描述线程池执行,我打个比喻:

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求
    【Java多线程】线程池(一)与线程池的初识_第37张图片
  • 当产品提个需求,正式员工(核心线程)先接需求(执行任务
  • 如果正式员工都有需求在做,即核心线程数已满),产品就把需求先放需求池(阻塞队列)。
  • 如果需求池(阻塞队列)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程)来做。
  • 如果所有员工(最大线程数也满了)都有需求在做了,那就执行拒绝策略
  • 如果外包员工把需求(任务)做完了,它经过一段(keepAliveTime)空闲时间,就离开公司了。

3.submit和execute区别

3.1.可以接受的任务类型

execute()

public void execute(Runnable command) 
  • execute只能接受Runnable类型的任务

submit()

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);   
  • submit()不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null

3.2.返回值

CallableRunnable的区别可知:

  • execute没有返回值
  • submit有返回值,所以需要返回值的时候必须使用submit

3.3.异常

1.execute()中抛出异常

  • execute()中的是Runnable接口的实现,所以只能使用try-catch来捕获CheckedException 或者 通过实现UncaughtExceptionHande接口处理UncheckedException,即和普通线程的处理方式完全一致

2.submit()中抛出异常

  • 不管提交的是Runnable还是Callable类型的任务,如果不对返回值Future调用get()方法,都会吃掉异常

Callable接口

//call能够抛出Exception异常,所以不管是CheckedException还是UncheckedException,直接抛出即可
public interface Callable<V> {
     
    V call() throws Exception;
}

测试代码

import java.util.concurrent.*;

public class ThreadExceptionTest {
     
    public static void main(String[] args) {
     
        ExecutorService executor = Executors.newCachedThreadPool();

        Future<Boolean> future = executor.submit(new CallableTask());
        try {
     
            future.get();
        } catch (InterruptedException | ExecutionException e) {
     
            e.printStackTrace();
        }

		executor.shutdown();//关闭线程池必须不能忘,否则主线程会一直阻塞
    }
}

class CallableTask implements Callable<Boolean> {
     
    public Boolean call() throws Exception {
     
//		InputStream in = new FileInputStream(new File("xx.pdf"));
        int num = 3 / 0;
        return false;
    }
}

【Java多线程】线程池(一)与线程池的初识_第38张图片

七.线程池异常处理

在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

1.在run方法中捕获代码可能抛出的所有异常

execute提交任务会抛出异常,submit提交任务不会抛出异常, 使用submit时需要使用try-catch捕获可能要产生的异常

@Test
public void testThreadPoolSubmitExceptionHandle() throws IOException, InterruptedException {
     
        ExecutorService singleThreadPool = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {
     
            singleThreadPool.submit(() -> {
     
                try {
     
                    Object object = null;
                    System.out.println(object.toString());
                    System.out.println("当前线程:" + Thread.currentThread().getName());
                } catch (Exception e) {
     
                    System.out.println("当前线程:" + Thread.currentThread().getName()+"发生异常");
                }
            });
        }

        //原因是因为做单元测试时跟WEB项目不同,线程还没有开始启动,主线程已经关闭,只要我们加入一段代码,让主线程不关闭,这样就可以跑子线程的方法了
        Thread.sleep(2000);
       singleThreadPool.shutdown();//gracefully shutdown
}

执行结果:
【Java多线程】线程池(一)与线程池的初识_第39张图片

2.通过Future对象的get方法接收抛出的异常

使用submit执行任务,可以利用返回的Future对象的get方法接收抛出的异常,然后进行处理

2.1.了解线程池submit()的执行流程

【Java多线程】线程池(一)与线程池的初识_第40张图片

submit提交任务的关键代源码

  //构造feature对象
  /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
     
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);//使用Future包裹Runnable
        execute(ftask);
        return ftask;
    }
     protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
     
        return new FutureTask<T>(runnable, value);
    }
     public FutureTask(Runnable runnable, V result) {
     
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
       public static <T> Callable<T> callable(Runnable task, T result) {
     
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }
    
    //线程池执行
     public void execute(Runnable command) {
     
        if (command == null)
            throw new NullPointerException();
               int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
     
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
     
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
    
    //捕获异常
    public void run() {
     
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
     
            Callable<V> c = callable;
            if (c != null && state == NEW) {
     
                V result;
                boolean ran;
                try {
     
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
     
                    result = null;
                    ran = false;
                    setException(ex);//设置异常
                }
                if (ran)
                    set(result);
            }
        } finally {
     
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }

通过以上分析

  • submit在执行过程中与execute不一样,不会抛出异常而是把异常保存在成员变量中,在FutureTask.get阻塞获取的时候再把异常抛出来
  • execute直接抛出异常之后线程就死掉了submit保存异常线程没有死掉,因此execute的线程池可能会出现任务丢失情况,因为线程没有得到重用。而submit不会出现这种情况

2.2.举例说明

@Test
public void testThreadPoolSubmitExceptionHandle() throws IOException, InterruptedException {
     
		 //创建一个固定长度的线程池
        ExecutorService fixedThreadPool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

        List<Future> futureList = new ArrayList<>();

        for (int i = 0; i < 5; i++) {
     
            futureList.add(fixedThreadPool.submit(() -> {
     
                System.out.println("当前线程:" + Thread.currentThread().getName());
                Object object = null;
                System.out.println(object.toString());
            }));
        }


        //原因是因为做单元测试时跟WEB项目不同,线程还没有开始启动,主线程已经关闭,只要我们加入一段代码,让主线程不关闭,这样就可以跑子线程的方法了
        boolean flag = false;
        do {
     
            flag = false;
            for (Future future : futureList) {
     
                //如果任务未完成,继续循环=>任务完成可能是 任务正常终止/异常/取消
                if (!future.isDone()) {
     
                    flag = true;
                }

                try {
     
                    future.get();
                } catch (ExecutionException e) {
     
                    e.printStackTrace();
                }
            }

            //果任务未完成,休眠10秒继续循环
            Thread.sleep(10);
        } while (flag);

        fixedThreadPool.shutdown();
}

执行结果:
【Java多线程】线程池(一)与线程池的初识_第41张图片

3.设置UncaughtExceptionHandler (不推荐)

为工作线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常

不推荐重写UncaughtExceptionHandler,因为UncaughtExceptionHandler 只有在execute.execute()方法中才生效,在execute.submit中是无法捕获到异常的

    @Test
    public void testSetUncaughtExceptionHandler() {
     
        //创建一个固定长度的线程池
        ExecutorService fixedThreadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(30), r -> {
     
            Thread t = new Thread(r);

            t.setUncaughtExceptionHandler(
                    (t1, e) -> {
     
                        System.out.println(t1.getName() + "-线程抛出的异常" + e);
                    });
            return t;
        });

        fixedThreadPool.execute(() -> {
     
            System.out.println(Thread.currentThread().getName());
            Object object = null;
            System.out.print("result## " + object.toString());
        });

        fixedThreadPool.shutdown();
    }

执行结果:
在这里插入图片描述

4.重写ThreadPoolExecutor

重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用


import java.util.concurrent.*;

/**
 * 重写ThreadPoolExecutor实现afterExecute方法处理异常
 */
public class CustomThreadPoolExecutorDemo {
     
    public static void main(String args[]) {
     

        //相当于 Executors.newFixedThreadPool(10)
        CustomThreadPoolExecutor service = new CustomThreadPoolExecutor();

        service.submit(new Runnable() {
     
            @Override
            public void run() {
     
                System.out.println(Thread.currentThread().getName());
                Object object = null;
                System.out.println(object.toString());
            }
        });

        service.shutdown();
    }
}

class CustomThreadPoolExecutor extends ThreadPoolExecutor {
     

    public CustomThreadPoolExecutor() {
     
        //调用父类构造方法实例化线程对象
        super(1, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1000));
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
     
        super.afterExecute(runnable, throwable);
        if (throwable == null && runnable instanceof Future<?>) {
     
            try {
     
                Object result = ((Future<?>) runnable).get();
                System.out.println(result);
            } catch (CancellationException ce) {
     
                throwable = ce;
            } catch (ExecutionException ee) {
     
                throwable = ee.getCause();
            } catch (InterruptedException ie) {
     
                Thread.currentThread().interrupt(); // ignore/reset
            }
        }

        if (throwable != null) {
     
            throwable.printStackTrace();
        }
    }
}

执行结果:
【Java多线程】线程池(一)与线程池的初识_第42张图片

5. 处理线程池异常的4种方法

  1. 在我们提供的Runnable的run方法中捕获任务代码可能抛出的所有异常
  2. 使用submit执行任务,利用返回的Future对象的get方法接收抛出的异常,然后进行处理
  3. 重写ThreadPoolExecutorafterExecute方法,处理传递到afterExecute方法中的异常
  4. 为线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常(submit不推荐)

八.线程池原理浅析

1.线程池组成部分

  • 线程池管理器
  • 工作线程
  • 任务队列
  • 任务

2.Executor家族

【Java多线程】线程池(一)与线程池的初识_第43张图片

  • Executor: 它是一个顶层接口,其他接口以及类都继承或实现于它,只有一个方法

    void execute(Runnable command);
    
  • ExecutorService: 它继承于Executor,是Executor的子接口,增加了一些常用的对线程的控制方法,之后使用线程池主要也是使用这些方法。

    如 shutdown(),isShutdown(),shutdownNow()

  • AbstractExecutorService: 是一个抽象类。ThreadPoolExecutor就是实现了这个类。

  • Executors: 这个类是一个工具类,里面包含一些创建线程池的方法

    Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具真正的线程池接口是 ExecutorService

3.线程池的线程复用的原理

源码分析

public void execute(Runnable command) {
     
    // 判断任务是否为空,为空就抛出异常
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    // 如果当前线程数小于核心线程数,就增加Worker
    if (workerCountOf(c) < corePoolSize) {
     
        // command就是任务,点击addWorker方法
        // 第二个参数用于判断当前线程数是否小于核心线程数
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 此时线程数大于等于核心线程数
    // 判断线程池是不是正在运行并将任务放到工作队列中
    if (isRunning(c) && workQueue.offer(command)) {
     
        // 再次检查线程状态
        int recheck = ctl.get();
        // 如果线程不是正在运行的,就删除掉任务并且拒绝
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)   //这里用于避免已经提交的任务没有线程进行执行
            addWorker(null, false);
    }
    // 如果任务无法添加或者大于最大线程数就拒绝任务
    else if (!addWorker(command, false))
        reject(command);
}

因为要查看的是Worker所以进入到addWorker()方法后点击Worker类查看runWorker()方法

w = new Worker(firstTask);

Worker 是ThreadPoolExecutor的内部类

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
final void runWorker(Worker w) {
     
    Thread wt = Thread.currentThread();
    // 获取到任务
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
     
        // 只要任务不为空或者能够获取到任务就执行下面的方法
        while (task != null || (task = getTask()) != null) {
     
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
     
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
     
                    // task是一个Runnable类型,调用run()方法就是运行线程
                    task.run();
                } catch (RuntimeException x) {
     
                    thrown = x; throw x;
                } catch (Error x) {
     
                    thrown = x; throw x;
                } catch (Throwable x) {
     
                    thrown = x; throw new Error(x);
                } finally {
     
                    afterExecute(task, thrown);
                }
            } finally {
     
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
     
        processWorkerExit(w, completedAbruptly);
    }
}

总结: 核心原理就是获取到task,如果task不为空就调用run()方法,这样就实现了线程的复用,达到让相同的线程执行不同任务的目的。

4.使用线程池的注意点

  • 避免任务的堆积(堆积容易产生内存溢出)
  • 避免线程数过多增加(缓存线程池会导致线程数过度增加)
  • 排查线程泄漏(线程已经执行完毕却无法被回收)

5.线程池特性

5.1.规则描述

线程池的线程执行规则跟任务队列有界或无界有很大的关系。

假设任务队列大小没有限制:

  1. 如果工作线程数量<=核心线程数量,直接启动一个核心线程来执行任务,不会放入队列中。
  2. 如果工作线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingDeque时,会将超过核心线程数的任务会放在任务队列中排队。
  3. 如果工作线程数量>核心线程数,但<=最大线程数,并且任务队列是SynchronousQueue时,线程池会创建非核心线程执行任务,且任务也不会被放在任务队列中。在任务完成后,空闲时间达到了超时时间就会被清除。
  4. 如果工作线程数量>核心线程数,并且>最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是没有设置大小的LinkedBlockingDeque,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。
  5. 如果工作线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue时,会因为线程池拒绝添加任务而抛出异常(RejectedExecutionException)。

假设任务队列大小有限制时:

  1. 如果工作线程数量>核心线程数,且任务队列是LinkedBlockingDeque时, 会将超过核心线程数的任务会放在任务队列中排队, 当任务队列塞满时,新增的任务会直接创建新线程(非核心线程)来执行,当创建的线程数量超过最大线程数量时会抛异常(RejectedExecutionException)。
  2. SynchronousQueue没有数量限制。因为他根本不保存这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常(RejectedExecutionException)。

5.2.规则验证

前提
下面所有的任务都是下面这样的,睡眠两秒后打印一行日志:

        Runnable runnable = new Runnable() {
     
            @Override
            public void run() {
     
                try {
     
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + " run");
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }

            }
        };

所有验证过程都是下面这样,执行三个任务,打印线程池信息,然后再执行三个任务,打印线程池信息,最后线程休眠8秒后,打印线程池信息.

        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        System.out.println("---先开三个---");
        System.out.println("核心线程数=>" + executor.getCorePoolSize() + ",线程池数=>" + executor.getPoolSize() + ",队列任务数=>" + executor.getQueue().size());

        executor.execute(runnable);
        executor.execute(runnable);
        executor.execute(runnable);
        System.out.println("---再开三个---");
        System.out.println("核心线程数=>" + executor.getCorePoolSize() + ",线程池数=>" + executor.getPoolSize() + ",队列任务数=>" + executor.getQueue().size());

        Thread.sleep(8000);
        System.out.println("----8秒之后----");
        System.out.println("核心线程数=>" + executor.getCorePoolSize() + ",线程池数=>" + executor.getPoolSize() + ",队列任务数=>" + executor.getQueue().size());

        //关闭线程池,不关闭的话由于主线程内一直存在6个核心线程,虚拟机不会关闭
        executor.shutdown();

5.2.1.验证1

核心线程数为6,最大线程数为10,超时时间为5秒,队列是SynchronousQueue

ThreadPoolExecutor executor = new ThreadPoolExecutor(6, 10, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

执行结果
【Java多线程】线程池(一)与线程池的初识_第44张图片

结论: 因为SynchronousQueue不保存任务,收到一个任务就去创建新线程。每个任务都是是直接启动一个线程来执行任务,一共创建了6个线程。8秒后线程池没有因为空闲时间被还是6个线程,因此核心线程默认情况下不会被回收,不受超时时间影响。

5.2.2.验证2

核心线程数为3,最大线程数为6。超时时间为5秒,队列是LinkedBlockingDeque

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());

执行结果
【Java多线程】线程池(一)与线程池的初识_第45张图片

结论:当任务数超过核心线程数时,会将超出的任务放在队列中,只会创建3个线程重复利用。

5.2.3.验证3

核心线程数为3,最大线程数为6,超时时间为5秒,队列是SynchronousQueue

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

执行结果
【Java多线程】线程池(一)与线程池的初识_第46张图片

结论:当队列是SynchronousQueue时,不会保存任务,超出核心线程的任务会创建新的线程来执行,看到一共有6个线程。但是这些线程中有3个是非核心线程,受超时时间影响,在任务完成后空闲超过5秒就会被回收。所以最后看到线程池还是只有三个线程。

5.2.4.验证4

  1. 核心线程数是3,最大线程数是4,超时时间为5秒,队列是LinkedBlockingDeque
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());

执行结果
【Java多线程】线程池(一)与线程池的初识_第47张图片
结论:LinkedBlockingDeque根本不受最大线程数影响

但是当LinkedBlockingDeque有大小限制时就会受最大线程数影响了(例2会说明)

  1. 核心线程数是3,最大线程数是4,队列是长度为2的LinkedBlockingDeque
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(2));

执行结果
【Java多线程】线程池(一)与线程池的初识_第48张图片
结论: 首先为三个任务开启了三个核心线程1,2,3,然后第四个任务和第五个任务加入到队列中,第六个任务因为队列满了,就直接创建一个新线程4,这是一共有四个线程,没有超过最大线程数。8秒后,非核心线程受到超时时间影响被回收了,因此线程池只剩3个线程。

当队列中的任务满了后,创建的线程 > 线程池最大线程数,默认情况下会拒绝处理任务,抛出RejectedExecutionException(例3会说明)

  1. 将队列大小设置为1
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(1));

执行结果
【Java多线程】线程池(一)与线程池的初识_第49张图片
异常信息详细如下
在这里插入图片描述

结论:直接出错在第6个execute方法上。因为核心线程是3个,当加入第四个任务的时候,就把第四个放在队列中。加入第五个任务时,因为队列满了,就创建新线程执行,创建了线程4。加入第六个线程时,也会尝试创建线程,但是因为已经达到了线程池最大线程数,所以直接抛异常RejectedExecutionException

5.2.5.验证5

核心线程数是3 ,最大线程数是4,队列是SynchronousQueue

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

执行结果
【Java多线程】线程池(一)与线程池的初识_第50张图片
异常信息详细如下
在这里插入图片描述
结论:这次在添加第五个任务时就报错了,因为SynchronousQueue不保存任务,收到一个任务就去创建新线程。所以第五个就会抛异常了。

相关好文

  • Java线程池实现原理及其在美团业务中的实践
  • 面试官一个线程池问题把我问懵逼了。
  • 如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。有坑
  • 填上面这个坑!再谈线程池动态调整那点事。
  • 突然就懵了!面试官问我:线程池中非核心线程是如何回收的?

你可能感兴趣的:(Java多线程,线程池,阻塞队列,工作线程,线程,多线程)