线程池内运行的线程抛异常,线程池会怎么办

简介: 线程池内运行的线程抛异常,线程池会怎么办

线程池中实际运行的是线程池自身的线程,只是在runWorker方法中调用了我们传递进入Runnable对象的run()方法,那么如果run()方法中出现异常了,那么要怎么处理?会不会将我们的线程池停掉?

我们先来看下runWorker()方法的具体逻辑:

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();
        } 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);
  }
}

在代码中我们看到,在调用用户Runnable实例方法run()的时候,进行了try…catch…finally,但是在catch()中是直接将异常抛出了,也就是说并未在while循环内消化掉,而是抛出给外层,这时会将while循环终止掉,然后在外层的try…finally中并未捕获内部传出的异常,所以异常信息会继续往上抛出,我们来关注一下这两层try的finally代码块,内部的finally中执行了一个空的方法afterExecute(),这个方法是留给我们自定义线程池时使用的,和beforeExecute()方法一样,既然是空方法,那我们就先不用去看它了,来看下外层的finally代码块

private void processWorkerExit(Worker w, boolean completedAbruptly) {
  // 从runWorker方法中传过来的是true,所以这句目前版本中必定会被执行到
  // 作用是将当前线程池中的有效线程数-1,意思也就是出现异常的线程会被从线程池中拿掉
  // 为什么说是出现异常的线程会被拿掉呢?因为在try内部是一个while循环,除非关闭核心线程或运行中线程出现异常,否则不会执行到这里
  if (completedAbruptly)
    decrementWorkerCount();
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    // 更新完成的任务数,只要是被线程池线程执行过的,不管是否出现异常,都被认为是执行成功的任务
    completedTaskCount += w.completedTasks;
    // 将当前Worker线程从线程池中移除销毁
    workers.remove(w);
  } finally {
    mainLock.unlock();
  }
  tryTerminate();
  // 一系列判断,主要是判断是否符合给线程池创建新的线程
  int c = ctl.get();
  if (runStateLessThan(c, STOP)) {
    if (!completedAbruptly) {
      int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
      if (min == 0 && ! workQueue.isEmpty())
        min = 1;
      if (workerCountOf(c) >= min)
        return;
    }
    // 给线程池创建新的线程,core之所以传递false,是因为这里要防止创建失败
    addWorker(null, false);
  }
}

通过源码我们看到在处理任务的过程中,如果线程出现异常,则会将该线程从线程池中移除销毁,然后再新创建一个线程加入到线程池中,也就是说在任务发生异常的时候,会终结掉运行它的线程。

我们从源码中得到的信息,现在来验证一下我们的分析

验证代码:

public static void main(String[] args) {
  ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30));
  for (int i = 0; i < 10; i++) {
    int finalI = i;
    Thread t = new Thread(() -> {
      System.out.println(10 / finalI - 5);
    });
    pool.execute(t);
  }
}

输出结果:

抛异常了,但是并未影响线程池中的其他任务,我们打断点在processWorkerExit()方法中,看下workers变量的数据

异常发生之前

异常发生之后

看到在异常前后,线程1f36e637被移除了,转而创建了一个7073cb62放到了线程池中,而未发生异常的线程578486a3依然存在于线程池中。

小结

通过示例我们验证了一点:当任务出现未被捕获到的异常时,会将执行该任务的线程池中的线程从线程池移除并结束掉,然后移除之后创建一个新的线程放回到线程池中。


上面我们知道了当线程执行的任务发生未被捕获的异常时,会将异常一直往上抛出,那么我们能否在主线程中捕获它进行处理呢?我们来试下

public static void main(String[] args) {
  ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30));
  for (int i = 0; i < 10; i++) {
    int finalI = i;
    Thread t = new Thread(() -> {
      System.out.println(10 / finalI - 5);
    });
    try {
      pool.execute(t);
    } catch (Exception e) {
      System.out.println("发生了异常");
    }
  }
}

运行结果

由运行结果可以看出我们并未捕获到线程池中线程抛出的异常,也就是异常并未被抛出到主线程中,这就尴尬了,毕竟这些异常是和业务相关联的,我们却无法捕获和处理,这咋整呢?

忽的一下,想到了线程池的比较重要的一个参数:ThreadFactory接口,这个接口的作用是按需创建新线程的,使用线程工厂消除了对Thread#Thread(Runnable) new Thread的强依赖,使应用程序能够使用特殊的Thread子类、优先级等。大白话就是让线程池中的线程使用我们自定义的线程,这个自定义可不是我们通过execute()或submit()传进来的自定义线程,而是Worker类中的thread变量,也就是实际运行的线程,我们看一下Worker类的构造方法

Worker(Runnable firstTask) {
  setState(-1); // inhibit interrupts until runWorker
  this.firstTask = firstTask;
  // 调用ThreadFactory的newThread方法创建线程
  this.thread = getThreadFactory().newThread(this);
}
在构造方法中调用线程工厂的newThread()方法创建运行线程,我们上面通过ThreadPoolExecutor的构造方法创建线程池时并未传入ThreadFactory参数,那么就会使用默认的Executors.defaultThreadFactory()来创建线程,它的实现逻辑如下:
public Thread newThread(Runnable r) {
  Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
  if (t.isDaemon())
    t.setDaemon(false);
  if (t.getPriority() != Thread.NORM_PRIORITY)
    t.setPriority(Thread.NORM_PRIORITY);
  return t;
}

那么我们要是想自定义Worker#thread的值的话,就自定义一个ThreadFactory实现类即可,比如我们可以把线程池创建语句升级为:

public static void main(String[] args) {
  ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30), r -> {
                    Thread t = new Thread(r);
                    t.setUncaughtExceptionHandler((t1, e) -> System.out.println("发生了异常"));
                    return t;
    });
  for (int i = 0; i < 10; i++) {
    int finalI = i;
    Thread t = new Thread(() -> {
      System.out.println(10 / finalI - 5);
    });
    pool.execute(t);
  }
}

我们使用lambda表达式来创建,使用Thread#setUncaughtExceptionHandler()方法来获取线程内未被捕获的异常,我们运行一下看看结果:

成功捕获了线程内部出现的异常。

那么现在就又有一个问题了:如果我们主动捕获并处理线程内抛出的异常,那么这个线程还会从线程池中移除销毁吗?

我们来试下,还是使用上面的那段代码,然后断点打在processWorkerExit()方法中,看下执行结果

异常之前

异常之后

从执行结果来看,发生异常的线程是35d176f7,在异常发生之后同样从线程池中被移除了。

总结

当线程池中线程执行任务的时候,任务出现未被捕获的异常的情况下,线程池会将允许该任务的线程从池中移除并销毁,且同时会创建一个新的线程加入到线程池中;可以通过ThreadFactory自定义线程并捕获线程内抛出的异常,也就是说甭管我们是否去捕获和处理线程池中工作线程抛出的异常,这个线程都会从线程池中被移除。

 

原文链接:

https://blog.csdn.net/luxiaoruo/article/details/106637384


目录
相关文章
|
2月前
线程CPU异常定位分析
【10月更文挑战第3天】 开发过程中会出现一些CPU异常升高的问题,想要定位到具体的位置就需要一系列的分析,记录一些分析手段。
78 0
|
1月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
112 38
|
27天前
|
Java
.如何根据 CPU 核心数设计线程池线程数量
IO 密集型:核心数*2 计算密集型: 核心数+1 为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
34 4
|
1月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
86 2
|
1月前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
96 4
|
1月前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
254 2
|
2月前
|
监控 Java
在实际应用中选择线程异常捕获方法的考量
【10月更文挑战第15天】选择最适合的线程异常捕获方法需要综合考虑多种因素。没有一种方法是绝对最优的,需要根据具体情况进行权衡和选择。在实际应用中,还需要不断地实践和总结经验,以提高异常处理的效果和程序的稳定性。
30 3
|
2月前
|
监控 Java
捕获线程执行异常的多种方法
【10月更文挑战第15天】捕获线程执行异常的方法多种多样,每种方法都有其特点和适用场景。在实际开发中,需要根据具体情况选择合适的方法或结合多种方法来实现全面有效的线程异常捕获。这有助于提高程序的健壮性和稳定性,减少因线程异常带来的潜在风险。
29 1
|
2月前
|
监控 API
Hook 线程与捕获线程执行异常
【10月更文挑战第11天】Hook 线程和捕获线程执行异常是多线程编程中不可或缺的技术。通过深入理解和掌握这些方法,我们可以提高程序的稳定性和可靠性,更好地应对各种异常情况。同时,在实际应用中要注意平衡性能和准确性,制定合理的异常处理策略,以确保程序的正常运行。
35 1
|
2月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
149 0
剖析Tomcat线程池与JDK线程池的区别和联系!