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

目录

核心代码

验证代码

小结

主线程能否捕获异常

总结


核心代码

线程池中实际运行的是线程池自身的线程,只是在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);
  }
}


输出结果:

线程池内运行的线程抛异常,线程池会怎么办_第1张图片

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

异常发生之前

线程池内运行的线程抛异常,线程池会怎么办_第2张图片

异常发生之后

线程池内运行的线程抛异常,线程池会怎么办_第3张图片

 看到在异常前后,线程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("发生了异常");
    }
  }
}

运行结果

线程池内运行的线程抛异常,线程池会怎么办_第4张图片

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

忽的一下,想到了线程池的比较重要的一个参数: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()方法来获取线程内未被捕获的异常,我们运行一下看看结果:

线程池内运行的线程抛异常,线程池会怎么办_第5张图片

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

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

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

异常之前

线程池内运行的线程抛异常,线程池会怎么办_第6张图片

 异常之后

线程池内运行的线程抛异常,线程池会怎么办_第7张图片

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

总结

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

线程池内运行的线程抛异常,线程池会怎么办_串一串cc的博客-CSDN博客

你可能感兴趣的:(并发编程,JDK源码,面试,java,算法,开发语言)