深度解析 Java 线程池

293 阅读21分钟

线程池是什么

  • 线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL
  • 线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务,这样做:
    • 一方面避免了处理任务时创建销毁线程开销的代价
    • 另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用

线程池有什么好处

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:开发人员可以对线程池进行扩展,比如延时定时线程池就允许任务延期执行或定期执行

线程池解决了什么问题

  • 线程池解决的核心问题就是资源管理问题
  • 在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题
    • 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
    • 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
    • 系统无法合理管理内部的资源分布,会降低系统的稳定性

线程池的架构

image.png

  • Executor 是顶层接口,提供了一种思想
    • 将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分
  • ExecutorService 接口增加了一些能力
    1. 扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法
    2. 提供了管控线程池的方法,比如停止线程池的运行
  • AbstractExecutorService 则是上层的抽象类
    • 将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可
  • ThreadPoolExecutor 实现类来实现最复杂的运行部分
    • ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务

线程池的主要组件

image.png

  1. 线程池管理器(ThreadPool):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务
  2. 工作线程(WorkThread):线程池中具体执行任务的线程,在没有任务时处于等待状态,可以循环的执行任务
  3. 任务接口(Task):用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度
  4. 任务队列(taskQueue):存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除,提供一种缓冲机制

线程池的创建

  • 通过 ThreadPoolExecutor 来创建线程池,其构造函数如下:
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
    
    
  • 参数解析
    1. corePoolSize:线程池中常驻的核心线程数
    2. maximumPoolSize:线程池中最大的线程数
    3. keepAliveTime:多余的空闲线程的存活时间,当线程池中的线程数量超过 corePoolSize 的时候,并且达到 keepAliveTime,则将多余的线程销毁直到数量等于 corePoolSize
    4. unit:是指定 keepAliveTime 的时候的时间单位
    5. workQueue:在等待被执行的任务队列
    6. threadFactory:生成线程的线程工厂,一般使用默认的即可
    7. handler:拒绝策略,当正在工作的线程等于 maximumPoolSize 且等候的任务也等于 workQueue 的大小时,如果还有新的任务进来则会拒绝

线程池的生命周期

  • ThreadPoolExecutor的运行状态有5种,分别为:
    • RUNNING
      • 该状态的线程池会接收新任务,并处理阻塞队列中的任务
    • SHUTDOWN
      • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务
    • STOP
      • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务
    • TIDYING
      • 该状态表明所有的任务已经运行终止,workCount(有效线程数)为 0
    • TERMINATED
      • 该状态表示线程池彻底终止
  • 运行状态转换 image.png

线程池的任务提交

  • 我们可以通过 execute() 或 submit() 两个方法向线程池提交任务,不过它们有所不同
  • execute() 方法:没有返回值,所以无法判断任务知否被线程池执行成功
    threadsPool.execute(new Runnable() {
        @Override
        public void run() {
        // TODO Auto-generated method stub
       }
    });
    
  • submit() 方法:返回一个 future 可以通过这个 future 来判断任务是否执行成功,通过 future.get() 方法来获取返回值
    try {
         Object s = future.get();
    } catch (InterruptedException e) {
       // 处理中断异常
    } catch (ExecutionException e) {
       // 处理无法执行任务异常
    } finally {
       // 关闭线程池
       executor.shutdown();
    }
    
    

线程池的任务执行

  • 线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用 execute() 添加一个任务时,线程池会按照以下流程执行任务 image.png
  1. 创建线程池后开始等待任务
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断
    • 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务
    • 如果正在运行的线程数量小于 corePoolSize,则会直接创建核心线程运行这个任务
    • 如果正在运行的线程数量大于等于 corePoolSize,则会将任务放入 workQUeue 中进行等待
    • 如果正在运行的线程数量大于等于 corePoolSize,且 workQueue 中已经存满任务
      • 如果正在运行的线程数量小于 maximumPoolSize,则会创建非核心线程来运行这个任务(注意不是阻塞队列中的任务)
      • 如果正在运行的线程数量大于等于 maximumPoolSize,则会执行拒绝策略来拒绝这个任务
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断
    • 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉
  5. 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小

线程池的任务队列

  • 线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务
  • 阻塞队列的特性
    • 当队列是空的,从队列中获取元素的操作将会被阻塞,直到其他线程往空的队列插入新的元素
    • 当队列是满的,从队列中添加元素的操作将会被阻塞,直到其他线程从满的队列取出元素后队列有空余空间
  • 下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素 image.png
  • 不同的队列可以实现不同的存去特性
    1. ArrayBlockingQueue:由数组结构组成的有界(自己指定)阻塞队列
    2. LinkedBlockingQueue:由链表结构组成的有界(大小默认值为integer.MAX_VALUE)阻塞队列
      • newFixedThreadPool线程池使用了这个队列
    3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列
    4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列
      • newScheduledThreadPool线程池使用了这个队列
    5. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
      • newCachedThreadPool线程池使用了这个队列
    6. LinkedTransferQueue:由链表组成的无界阻塞队列
    7. LinkedBlockingDeque:由链表组成的双向阻塞队列

线程池的拒绝策略

  • 什么是拒绝策略
    • 线程池中的正在运行的线程数量达到了 maximumPoolSizee,同时 workQueue 已经排满了,再也塞不下新任务了这个是时候我们就需要拒绝策略机制合理的处理这个问题
  • 什么时候会执行拒绝策略
    • 总任务数 > maximumPoolSize + workQueue.size
    • 所以说当任务队列是一种无解队列时则不会触发拒绝策略
  • 拒绝策略的种类
    1. AbortPolicy(默认):直接抛出 RejectedExecutionException 异常阻止系统正常运行
    2. CallerRunsPolicy:既不会抛弃任务,也不会抛出异常,而是由调用线程(提交任务的线程,主线程)处理该任务
    3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后提交新来的任务
    4. DiscardPolicy:丢弃任务,但是不抛出异常。如果允许任务丢失,这是最好的一种策略

几种常用的线程池

  • Java 通过 Executors 提供四种线程池,其内部都是通过 ThreadPoolExecutor 实现的
    • newFixedThreadPool (固定数目线程的线程池)
    • newCachedThreadPool(可缓存线程的线程池)
    • newSingleThreadExecutor(单线程的线程池)
    • newScheduledThreadPool(定时及周期执行的线程池)
newFixedThreadPool
  • 构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 线程池特点
    • corePoolSize 和 maximumPoolSize 大小是一样的,所以固定大小的线程池
    • 没有所谓的非空闲时间,即 keepAliveTime 为 0
    • 阻塞队列为无界队列 LinkedBlockingQueue
  • 工作机制
    • 提交任务
    • 如果线程数少于核心线程,创建核心线程执行任务
    • 如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞队列
    • 如果线程执行完任务,去阻塞队列取任务,继续执行
  • 代码案例
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建一个固定大小的线程池
        ExecutorService executors = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            executors.execute(new Task(i + ""));
        }
        // 关闭线程池
        executors.shutdown();
    }
}

class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println(e);
        }
        System.out.println("end task " + name);
    }
}
  • 执行结果 image.png
  • 结果分析
    • 一次性放入 6 个任务,由于线程池只有固定的 4 个线程,因此前 4 个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务
  • 扩展讨论:使用 submit 和 Callable 来获取任务的返回值
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建一个固定大小的线程池
        ExecutorService executors = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            Future<?> future = executors.submit(new Task2(i + ""));
            System.out.println(future.get());
        }
        // 关闭线程池
        executors.shutdown();
    }
}

class Task2 implements Callable<String> {
    private final String name;

    public Task2(String name) {
        this.name = name;
    }

    @Override
    public String call() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println(e);
        }
        System.out.println("end task " + name);
        return "Callable 的 run 方法返回值";
    }
}
  • 执行结果 image.png
  • 适用场景
    • 适用于处理 CPU 密集型的任务,确保 CPU 在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务
newCachedThreadPool
  • 构造函数
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
  • 线程池特点
    • 核心线程数为 0
    • 最大线程数为 Integer.MAX_VALUE
    • 阻塞队列是 SynchronousQueue
    • 非核心线程空闲存活时间为 60 秒
  • 工作机制
    • 提交任务
    • 因为没有核心线程,所以任务直接加到SynchronousQueue队列
    • 判断是否有空闲线程,如果有则取出任务执行
    • 如果没有空闲线程则新建一个线程执行
    • 执行完任务的线程,还可以存活60秒,如果在这期间接到任务可以继续活下去;否则被销毁
  • 代码案例
public class newCachedThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService executors = Executors.newCachedThreadPool();
        for (int i = 0; i < 6; i++) {
            executors.execute(new Task(i + ""));
        }
        // 关闭线程池
        executors.shutdown();
    }
}
  • 执行结果 image.png
  • 结果分析
    • 由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以 6 个任务可一次性全部同时执行
  • 适用场景
  • 用于并发执行大量短期的小任务
ScheduledThreadPool
  • 还有一种任务需要定期反复执行,例如每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用 ScheduledThreadPool
  • 构造函数
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
  • 线程池特点
    • 通过 ScheduledThreadPoolExecutor 去创建线程池
    • 最大线程数为 Integer.MAX_VALUE
    • 阻塞队列是 DelayedWorkQueue
    • keepAliveTime 为 0
  • 工作机制
    • 添加一个任务
    • 线程池中的线程从 DelayQueue 中取任务
    • 线程从 DelayQueue 中获取 time 大于等于当前时间的task
    • 执行完后修改这个 task 的 time 为下次被执行的时间
    • 这个 task 放回 DelayQueue 队列中
  • 代码案例1:提交一次性任务,它会在指定延迟后只执行一次
public class newScheduledThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(4);
        // 提交一次性任务,它会在指定延迟后只执行一次
        scheduledExecutorService.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
        // 定时任务不能关闭线程池
        // scheduledExecutorService.shutdown();
    }
}
  • 执行结果

image.png

  • 代码案例2:任务以固定的每 1 秒执行
public class newScheduledThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(4);
        // 任务以固定的每3秒执行,我们可以这样写
        scheduledExecutorService.scheduleAtFixedRate(new Task("fixed-rate"), 2, 1, TimeUnit.SECONDS);
        // 关闭线程池
        // scheduledExecutorService.shutdown();
    }
}
  • 执行结果

image.png

  • 代码案例3:任务以固定的 3 秒为间隔执行
public class newScheduledThreadPoolTest {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(4);
        // 任务以固定的每3秒执行,我们可以这样写
       scheduledExecutorService.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
        // 关闭线程池
        // scheduledExecutorService.shutdown();
    }
}
  • 执行结果

image.png

  • FixedRate 和 FixedDelay 的区别
    • FixedRate 是指任务总是以固定时间间隔触发,不管任务执行多长时间
    • FixedDelay 是指上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务
  • 思考
    • Q:在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?
    • A:不会,FixedRate 每次任务结束后会从任务队列中找下一次该执行的任务,判断是否到时机执行。FixedRate 的任务某次执行时间再长也不会造成两次任务实例同时执行
    • Q:如果任务抛出了异常,后续任务是否继续执行?
    • 不会,会阻塞在那里
  • 适用场景
    • 适用周期性任务
newSingleThreadExecutor
  • 构造方法
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 线程池特点
    • 核心线程数为1
    • 最大线程数也为 1
    • 阻塞队列是 LinkedBlockingQueue
    • keepAliveTime 为 0
  • 工作机制
    • 提交任务
    • 线程池是否有一条线程在,如果没有则新建线程执行任务
    • 如果有则将任务加到阻塞队列
    • 当前的唯一线程,从队列取任务,执行完一个,再继续取
  • 适用场景
    • 适用于串行执行任务的场景,一个任务一个任务地执行
关于上面四种线程池的相关面试题
  • Q:使用无界队列的线程池会导致内存飙升吗?
    • 会的,newFixedThreadPool 使用了无界的阻塞队列 LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致 OOM
  • Q:说说几种常见的线程池及使用场景?
    • 先列举常见线程池:newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool,newScheduledThreadPool
    • 再从线程池特点、工作机制、使用场景进行描述
    • 最后分析可能存在的问题
      • newFixedThreadPool 和 newSingleThreadPool 允许的最长的等待队列长度均为 Integer.MAX_VALUE,可能会导致堆积大量的请求,导致 OOM
      • newCachedThreadPool 和 newScheduledThreadPool 允许创建的线程数均为 Integer.MAX_VALUE,可能会导致堆积大量的线程,导致 OOM

线程池的异常处理

  • 使用 submit 提交任务
public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        threadPool.submit(() -> {
            System.out.println("current thread name" + Thread.currentThread().getName());
            Object object = null;
            System.out.print("result## "+object.toString());
        });
    }
}
  • 很明显这段代码会有异常,但是线程池照样能正常运行

image.png

  • 使用 execute 提交任务
public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        threadPool.execute(() -> {
            System.out.println("current thread name" + Thread.currentThread().getName());
            Object object = null;
            System.out.print("result## "+object.toString());
        });
    }
}
  • 线程池同样正常运行,但是抛出了异常

image.png

四种处理方式
  • 方式一:使用 try-catch,submit 和 execute 方式提交任务都可捕获到
public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        threadPool.submit(() -> {
            System.out.println("current thread name" + Thread.currentThread().getName());
            try {
                Object object = null;
                System.out.print("result## "+object.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}
  • 方式二:每一个任务都加一个try-catch 实在是太麻烦了,则可以使用 UncaughtExceptionHandler 这个类,注意只能捕获 execute 方式产生的异常
public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        Thread thread = new Thread(() -> {
            System.out.println("current thread name" + Thread.currentThread().getName());
            Object object = null;
            System.out.print("result## " + object.toString());
        });
        thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {
            System.out.println("exceptionHandler:" + e.getMessage());
        });
        threadPool.execute(thread);
    }
}
  • 因此如果我们不想在每个线程的任务里面都加 try-catch 的话,可以用自己实现的一个线程池,传入我们自己重写的 ThreadFactory,该线程工厂在创建线程的时候都赋予 UncaughtExceptionHandler 处理器对象
public static void main(String[] args) {

    ThreadFactory factory = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            // 创建一个线程
            Thread t = new Thread(r);

            // 给创建的线程设置UncaughtExceptionHandler对象 里面实现异常的默认逻辑
            t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> {
                System.out.println("线程工厂设置的exceptionHandler:" + e.getMessage());
            });
            return t;
        }
    };

    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), factory);
    executor.execute(()-> {
        System.out.println(1 /0);
    });
}

image.png

  • 方式三:submit 和 execute 的一个区别就是 submit 是有返回值的,那么我们可以通过 submit 的返回值 Future 来捕获异常,注意如果要获取返回的结果的话,这里的线程池提交的参数是 Callable 类型
public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(1);

    // 创建Callable对象,会抛出异常
    Callable<Integer> callable = (() -> {
        return 1 / 0;
    });

    // 提交 Callable 进线程池,返回 Future
    Future future = executorService.submit(callable);

    try {
        // 获取线程池里面的结果
        Integer a = (Integer) future.get();
        System.out.println("future 中获取结果:" + a);
    } catch (Exception e) {
        // 获取线程池里面的异常
        System.out.println("future中获取异常:" + e.getMessage());
    }

    executorService.shutdown();
}

image.png

  • 如果是 return 1 / 1,则是获取结果:1
  • 方式四:重写 ThreadPoolExecutor 的 afterExecute 方法,处理传递的异常引用
// 代码在最后来写,因为涉及到了后文的内容后才可以理解
  • 小结
    • 通过使用 try-catch 捕获 submit 和 execute 方式的异常
    • 通过重写线程工厂的方法,在该线程工厂在创建线程的时候都赋予 UncaughtExceptionHandler 处理器对象,这样可以捕获到 execute 方式的异常
    • 通过 submit 的返回值 future.get() 方法来获取异常信息,注意这种方式的线程只能是 Callable 类型的
    • 通过重写 ThreadPoolExecutor的afterExecute 方法
思考
  • 回到最初的问题,为什么使用 submit 提交有异常的任务不会抛出异常,但是使用 execute 提交有异常的任务会抛出异常呢?
  • 我们来看看 submit 和 execute 方法的具体实现
  • submit 方法
    • 接口:ExecutorService
    <T> Future<T> submit(Runnable task, T result);
    
    • 实现类:AbstractExecutorService
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
    
    • submit 方法内部也是调用了 execute 方法。调用之前创建了一个 RunableFuture 对象,而且将这对象作为参数传入了 execute 方法中, 并且在执行完 execute 后,将了这个 RunableFuture 作为返回值
    • 通过下面可以看到,RunableFuture 同时继承了 Runable 接口和 Future 接口
    public interface RunnableFuture<V> extends Runnable, Future<V> {
        /**
         * Sets this Future to the result of its computation
         * unless it has been cancelled.
         */
        void run();
    }
    
    • newTaskFor 创建的是一个 FutureTask 任务,这个很重要,两个方法的差异就体现在这个点上,后文会讲到
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    
    • 那么现在问题就明朗了,submit 内部也是调用的 execute 完成的,只是传入了一个 RunnableFuture 类型的参数,问题就出在了 execute 方法内部,我们看看 execute 内部的实现
  • execute 方法
    • 接口:Executor
    void execute(Runnable command);
    
    • 实现类:ThreadPoolExecutor
    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);
    }
    
    • 在线程池的 excute 方法里面,我们的任务被提交到了 addWorker(command, true),我们看下 addWorker 内部的实现
    private boolean addWorker(Runnable firstTask, boolean core) {
        // ...... 省略
        try {
            // 重点:将任务封装成了一个 Worker 对象
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
    
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        // 将 worker 加到线程池的队列中
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    // 启动线程池中的一个线程 
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
    
    • 任务被封装了一个 worker,而 worker 实现了 Runable 接口,因此执行的逻辑就在 worker 的 run 方法里面
    public void run() {
        runWorker(this);
    }
    
    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 的 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 和异常
                        // 这也就是为什么【方式四】可以实现的原因
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
    
    • 核心就在 task.run() 这个方法里面了, 期间如果发生异常会被抛出
    • 小结一下
      • 如果用 execute 方法提交的任务,会被封装成了一个 Runable 任务,然后进去再被封装成一个 Worker 对象,最后在 Worker 的 run 方法里面执行 runWoker 方法, 里面再获取到我们最初自己的任务执行 run 方法,并且用 try-catch 捕获了异常,会被直接抛出去,因此我们在 execute 方法中看到了我们的任务的异常信息
      • 那么为什么 submit 方法没有异常信息呢? 因为 submit 方法是将任务封装成了一个 FutureTask 任务,传入 execute 方法中,然后这个 FutureTask 被封装成 Worker,在 Woker 的 run 方法里面最终调用的是FutureTask 的 run 方法
      • 即 execute 运行的是 Runnable 接口的实现类中的 run 方法,submit 运行的是 RunnableFuture 的实现类 FutureTask 中的 run 方法
    • 那么问题就出在了这个 run 方法上,下面我们看下 FutureTask 类中 run 方法的实现
    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);
        }
    }
    
    • setException 方法
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }
    
    • 将异常对象赋予 outcome,outcome 是 FutureTask 的返回结果,调用 Futuretask 对象的 get 方法的时候,返回 report(),reoport 方法里面实际上返回的是 outcome,刚好之前的异常就 set 到了这个 outcome 里面
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
    
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }
    
    • 因此在用 submit 方法提交的时候,Runable 对象被封装成了FutureTask ,里面的 run 方法 try-catch 了所有的异常,并设置到了 outcome 对象里面, 可以通过 future.get() 获取到 outcome,所以在 submit 方法提交的时候里面发生了异常是不会有任何抛出信息的
    • 那么在submit里面,除了从返回结果里面取到异常之外, 没有其他方法了。
    • 因此在不需要返回结果的情况下,最好用 execute ,这样如果疏漏了异常捕获也不至于丢掉异常信息
  • 方式四:重写 ThreadPoolExecutor 的 afterExecute 方法,处理传递的异常引用
  • execute 方式提交
public static void main(String[] args) {

    // 1. 创建一个自己定义的线程池,重写afterExecute方法
    ExecutorService service = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(10)) {
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            System.out.println("afterExecute 里面获取到异常信息:" + t.getMessage());
        }
    };

    // 2. 提交任务
    service.execute(() -> {
        int i = 1 / 0;
    });

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

image.png

  • submit 方式提交
  • 如果要用这个 afterExecute 处理 submit 提交的异常则要额外处理,因为用 submit 提交的时候里面的Throwable 对象为 null,需要在Runnable r里面取,此时这个 r 实际的类型是 FutureTask
public static void main(String[] args) {

    // 1. 定义线程池
    ExecutorService service = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(10)) {

        // 2. 重写 afterExecute 方法
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            if (t != null) {  // 这个是 execute 提交的时候
                System.out.println("afterExecute 里面获取到异常信息" + t.getMessage());
            }

            // 3. 如果r的实际类型是 FutureTask,那么是 submit 提交的,所以可以在里面 get 到异常
            if (r instanceof FutureTask) {
                try {
                    Future<?> future = (Future<?>) r;
                    future.get();
                } catch (Exception e) {
                    System.out.println("future里面取执行异常:" + e);
                }
            }
        }
    };

    // 3. 提交任务
    service.submit(() -> {
        int i = 1 / 0;
    });

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

image.png

参考与感谢