线程池异常如何处理?

前言

能搜到本博文的粉丝们,肯定对这个标题已经很熟悉了,但是标题新颖不是决定一篇博文的质量所在,而是内容,线程池在项目中用法太多,在java行业无处不在,如果连线程池都不知道,我觉得在市场上连入门级都算不上,所以我这边博文主要围绕量大维度去展开编写,至于线程池是什么 如何使用,我这篇博文不会去叙说

大家在开发的过程中是否发现,我们使用线程池的时候很少去处理运行过程中出现的错误,不处理错误这样没关系吗?不处理会不会导致线程池结束?如果需要处理错误我们应该如何进行处理呢?那么今天从以下几个方面来看一下

基础知识

类继承图

下面如果有贴出源码,对应的源码是JDK8
主要的源码类
java.util.concurrent.ThreadPoolExecutor、
java.util.concurrent.ThreadPoolExecutor.Worker
java.util.concurrent.AbstractExecutorService
在这里插入图片描述

线程池的状态

在这里插入图片描述

线程池异常的分类

这个标题指的线程池异常指的是:线程池内部线程发生的异常和任务量过大导致的拒绝策略发生的异常
如何处理?:指的是发生异常了我们的原因是什么?如何去解决?

在这里插入图片描述

线程池原理

在解释上面的现象之前,大家必须要熟悉线程池运行的原理,这个不是八股文,很重要的,如果不知道源码根本不知道异常发生的时机和解决方案:请大家跳转到我之前的一篇博文,有详细介绍
线程池原理

线程池运行的线程和队列中等待的线程是同一个吗?

其实这个问题就有歧义:
线程池运行的线程和队列中等待的“线程”不是同一个概念。运行的线程是线程池管理的工作者线程,它们负责从队列中取出并执行任务;而等待队列中的则是待执行的任务实体,并非线程本身

线程池运行的线程和队列中等待的并非“线程”,而是等待执行的任务。这里有一个概念上的区别:

  • 线程池运行的线程:指的是线程池中实际执行任务的工作者线程。这些线程由线程池创建和管理,用于执行提交到线程池的任务。当一个任务分配给一个线程后,该线程会执行该任务直至完成,然后继续从任务队列中获取下一个任务(如果有的话)进行执行。这些工作者线程在整个线程池的生命周期内可以被重复利用,执行多个不同的任务。

  • 队列中等待的:实际上等待的是任务而非线程。当所有的工作者线程都在忙于执行任务,而新的任务被提交到线程池时,这些新任务会被放置在一个阻塞队列中等待,直到某个线程变得可用来处理它们。

线程池异常

Runable执行异常(业务异常)

执行execute方法发生的异常

首先我们知道线程池原理中有个重要的方法: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("发生了异常");
    }
  }
}

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

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

解决方案一:run方法异常捕获

在Runnable的run方法内部使用try-catch块来捕获异常是一种常见做法,这可以确保线程在遇到问题时不会突然中断,同时给予开发者处理异常的机会。下面是一个简单的例子展示了如何在run方法中使用try-catch:

public class ExceptionHandlingTask implements Runnable {
    private int numerator;
    private int denominator;

    public ExceptionHandlingTask(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    @Override
    public void run() {
        try {
            // 尝试执行可能会抛出异常的操作
            int result = numerator / denominator;
            System.out.println(numerator + " / " + denominator + " = " + result);
        } catch (ArithmeticException e) {
            // 捕获并处理除以零异常
            System.err.println("发生错误:尝试除以零");
        } catch (Exception e) {
            // 捕获其他类型的异常
            System.err.println("发生未知错误:" + e.getMessage());
            e.printStackTrace(); // 打印堆栈跟踪,有助于调试
        } finally {
            // (可选)finally块中放置需要确保执行的清理代码
        }
    }
}

// 使用示例
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++) {
        pool.execute(new ExceptionHandlingTask(10, i)); // 注意:当i为0时,会在run方法内部被捕获异常
    }
    
    pool.shutdown();
    // 可以进一步添加对pool.shutdown()之后的处理,如awaitTermination等
}

在这个例子中,ExceptionHandlingTask类实现了Runnable接口,并在其run方法内部使用了try-catch结构来捕获可能发生的ArithmeticException(比如除以零时)。通过这种方式,即使任务执行中出现异常,线程也不会意外终止,而是按照开发者定义的方式处理异常,并且线程池可以继续处理其他任务。

因为在run方法里面已经处理了异常,那么就不会往上抛了也不会退出while循环,看下面的截图,task.run()这个方法就不会往外面抛,就当做正常执行,但是注意的是不是在调用execute()方法进行try。。。catch,因为run方法其实就是你的业务逻辑,如果有异常肯定是进入了截图中的catch了,execute()是截图这个方法的最外层了,外面肯定是捕获不住的,因为异常到不了,已经在截图这个地方就catch住了
在这里插入图片描述

解决方案二:重写afterExecute方法

刚刚在方案一的地方其实也说了如果你的run方法的业务逻辑中并未捕获,那么就会进入afterExecute这个方法,那么这个方法其实是个空方法,你需要自定义的,否则也会继续往上抛的

重写ThreadPoolExecutor的afterExecute(Runnable r, Throwable t)方法是另一种捕获和处理线程池中任务执行后异常的策略。这个方法在线程池执行完一个任务后被调用,无论任务是否成功完成。它接收两个参数:刚执行完的任务(Runnable r)和该任务执行过程中抛出的异常(如果有,否则为null)。通过重写这个方法,你可以集中处理任务执行后的清理工作及异常情况。

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

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t != null) {
            // 这里可以处理任务执行时抛出的异常
            System.err.println("任务执行时发生异常: " + t.getMessage());
            // 可以添加日志记录、报警或其他补偿措施
        }
        // 这里还可以添加其他清理或监控代码
    }
}

public static void main(String[] args) {
    CustomThreadPoolExecutor pool = new CustomThreadPoolExecutor(
        2, 5, 10, TimeUnit.MILLISECONDS, 
        new ArrayBlockingQueue<>(30)
    );
    
    for (int i = 0; i < 10; i++) {
        int finalI = i;
        pool.execute(() -> {
            try {
                System.out.println(10 / finalI - 5);
            } catch (Exception e) {
                // 虽然这里也捕获了异常,但为了演示afterExecute,我们让它抛出
                throw new RuntimeException(e);
            }
        });
    }
    
    pool.shutdown();
}

在这个示例中,我们创建了一个CustomThreadPoolExecutor类继承自ThreadPoolExecutor,并重写了afterExecute方法来捕获并处理任务执行时抛出的异常。注意,在任务执行逻辑中故意让异常向上抛出,以便在afterExecute中处理。实际应用中,根据需要可以在run方法内部处理异常,也可以依赖afterExecute进行统一处理,或者两者结合使用

解决方案三:自定义异常处理器

忽的一下,想到了线程池的比较重要的一个参数: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);
  }
}

上面的样例只是一个样例,实际项目中可以换个写法显的更加优雅点,比如下面的写法

自定义异常处理器

实现Thread.UncaughtExceptionHandler接口,并重写uncaughtException()方法来定义你自己的异常处理逻辑。例如,你可以记录异常日志、发送警报或采取其他适当的操作。

public class CustomExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 自定义异常处理逻辑
        System.out.println("Exception occurred in thread: " + t.getName());
        e.printStackTrace();
        // 其他处理操作...
    }
}
在创建线程池时设置异常处理器

在创建线程池时,使用ThreadPoolExecutor的构造函数之一,并传递自定义的异常处理器。

Thread.UncaughtExceptionHandler exceptionHandler = new CustomExceptionHandler();
ExecutorService executorService = new ThreadPoolExecutor(
    corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(), exceptionHandler
);

这样,当线程池中的线程执行任务时发生异常,就会调用自定义的异常处理器进行处理

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

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

异常之前
在这里插入图片描述
异常之后
在这里插入图片描述

小结:从执行结果来看,发生异常的线程是35d176f7,在异常发生之后同样从线程池中被移除了。如果我们主动捕获并处理线程内抛出的异常,线程仍然会从线程池中移除并销毁的

执行submit方法发生的异常

首先我们看下面的一个例子

测试流程
测试用例
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
 
public class ExecutorsTest {
 
    public static void main(String[] args) throws Exception {
        ThreadPoolTaskExecutor executor = init();
        executor.execute(() -> sayHi("execute"));
        Thread.sleep(1000);
        executor.submit(() -> sayHi("submit"));
    }
 
    public static void sayHi(String name) {
        String printStr = "thread-name:" + Thread.currentThread().getName() + ",执行方式:" + name;
        System.out.println(printStr);
        throw new RuntimeException(printStr + " error!!!");
    }
 
    private static ThreadPoolTaskExecutor init() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("thread_");
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(1000);
        executor.setKeepAliveSeconds(30);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();;
        return executor;
    }
}

抛出堆栈异常为啥对了一半?
在这里插入图片描述

从执行结果我们看出
当执行方式是execute时,可以看到堆栈异常的输出。
当执行方式是submit时,堆栈异常没有输出。

怎么拿到submit的异常堆栈?

在这里插入图片描述
所以,现在知道为什么回答:抛出堆栈异常只对了一半吧。
execute方法执行时,会抛出(打印)堆栈异常。
submit方法执行时,返回结果封装在future中,如果调用future.get()方法则必须进行异常捕获,从而可以抛出(打印)堆栈异常。
你以为这一部分写到这里就完事了?那不行啊,你心里没有一个疑问吗?为啥execute直接抛出异常,submit没有直接抛出异常呢?

源码查看
执行executes方法时

在java.util.concurrent.ThreadPoolExecutor#runWorker中抛出了异常:
在这里插入图片描述
在_java.lang.ThreadGroup#uncaughtException_进行了异常处理:
在这里插入图片描述
这个uncaughtException是何许人也,看java doc上咋说的:
在这里插入图片描述
这个方法是JVM调用的,我们只需要指定我们想要的处理方式即可。
那我们怎么指定呢

//直接new Thread()的时候
Thread t=newThread();
t.setUncaughtExceptionHandler(newThread.UncaughtExceptionHandler()
{
    public void uncaughtException(Thread t, Throwable e){
    //根据业务场景,做你想做的 }
});
//线程池的时候
ExecutorService threadPool = Executors.newFixedThreadPool(1, thread -> {
Thread t =newThread(thread);
t.setUncaughtExceptionHandler((t1, e) ->
System.out.println("根据业务场景,做你想做的:"+ e.getMessage()));return;}
);

这个其实在上面的解决方案中也已经给出了,这个地方也重新提及下

执行submit方法时

在这里插入图片描述

其本质也是调用了execute方法,所以它还是回到_java.util.concurrent.ThreadPoolExecutor#runWorker_方法:
在这里插入图片描述

向前,继续跟进去看看:
在这里插入图片描述
_java.util.concurrent.FutureTask#setException_干啥了啊,瞅一眼:
在这里插入图片描述
我们马上走向最终的真相:
在这里插入图片描述

真相出来了,首先呢,execute呢,如果run方法执行的时候是直接抛出去的,而submit呢,在run方法里面进行了包装,在get的时候才会抛出,那么抛出的解决方案就和上面提及到的解决方案一样了,只不过一个在run方法也就是执行业务体执行的时候进行了try…catch,一个呢需要在调用submit.get()才可以进行try…catch

线程池触发了拒绝策略抛出的异常

这个拒绝策略既包含默认的也包含自定义的拒绝抛出的异常
在这里插入图片描述

看上面的execute方法的主体流程,可以看到上面说的无论捕获异常解决方案比如在run方法的业务体上进行try…catch进行捕获等方法,如果发生了拒绝策略抛出的异常肯定是捕获不到的,因为此时都没有进入addWorker方法,也就进入不到run方法中,在最外层就抛出去了,那么这种情况如何解决呢?

为了处理或监控由 AbortPolicy 抛出的异常,你可以采取以下策略:

  1. 自定义拒绝策略:最直接的方法是不使用 AbortPolicy,而是实现一个自定义的 RejectedExecutionHandler,在其中你可以捕获异常并采取相应的处理措施,比如记录日志、将任务移到另一个队列等待后续处理等。
  2. 外部监控与日志:即使使用默认的 AbortPolicy,也可以通过强大的日志系统来捕获和记录由线程池生成的日志条目,包括拒绝执行时的异常信息。确保你的日志级别设置得当,以便记录这些异常。
  3. 任务提交封装:在调用 execute() 的地方,你可以考虑使用一个包装方法,该方法尝试执行任务并在外部捕获可能的 Throwable(虽然直接由 execute() 抛出的异常不会到达这里,但可以作为一种逻辑上的错误处理包裹)。然而,这主要是为了处理其他层次的异常,对于直接由 AbortPolicy 抛出的异常,这个方法的直接捕获并不适用。
  4. 使用Future和Callable:如果可能,改为使用 submit(Callable) 方法提交任务,并使用返回的 Future 对象来检查任务是否成功完成。虽然这不直接解决 AbortPolicy 异常捕获的问题,但它提供了一种机制来得知任务是否执行成功或失败(通过 Future.get() 方法可能会抛出 ExecutionException,其中包含了任务执行时的异常)。
  5. 设计容错逻辑:在任务设计层面,考虑任务的幂等性和重试逻辑。即使任务被拒绝,也可以在应用层通过某种逻辑来重新提交任务或采取其他补救措施。

综上,处理 AbortPolicy 异常的关键在于提前规划和异常管理策略,而不是在调用 execute()
的地方直接捕获这个异常。通过自定义拒绝策略和增强监控,你可以有效地管理任务拒绝的情况。

自定义拒绝策略

创建自定义 RejectedExecutionHandler:

当你创建 ThreadPoolExecutor 时,可以通过构造函数传递一个自定义的 RejectedExecutionHandler。在这个类中,你可以定义当任务被拒绝时需要执行的操作,比如记录日志、统计拒绝次数、或者尝试其他的处理方式。

class MyRejectHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 在这里处理拒绝的任务
        System.out.println(r.toString() + " 被拒绝执行!");
        // 可以添加日志记录、告警、或者备用处理逻辑
    }
}

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,
        maximumPoolSize,
        keepAliveTime,
        TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(),
        new MyRejectHandler()); // 使用自定义拒绝策略

ThreadPoolExecutor.AbortPolicy 是线程池的默认拒绝策略,当任务被拒绝时,它会直接抛出一个未检查的 RejectedExecutionException 异常。由于 execute(Runnable command) 方法本身不捕获这个异常,所以在直接调用 execute() 的地方无法直接捕获到这个异常

总结

  • execute源码执行流程
    1、开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增coreThread去执行任务)。执行到task.run()时会去执行提交的任务。
    如果任务执行失败,或throw x抛出异常。
    2、之后会到finally中的afterExecute()扩展方法,我们可以扩展该方法对异常做些什么。
    3、之后因为线程执行异常会跳出runWorker的外层循环,进入到processWorkerExit()方法,此方法会将执行任务失败的线程删除,并新增一个线程。
    4、之后会到ThreadGroup#uncaughtException方法,进行异常处理。
    如果没有通过setUncaughtExceptionHandler()方法设置默认的UncaughtExceptionHandler,就会在uncaughtException()方法中打印出异常信息。
  • submit源码执行流程
    1、将传进来的任务封装成FutureTask,同样走execute的方法调用,然后直接返回FutureTask。
    2、开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增coreThread去执行任务)。
    3、执行到task.run()时,因为是FutureTask,所以会去调用FutureTask.run()。
    4、在FutureTask.run()中,c.call()执行提交的任务。如果抛出异常,并不会throw x,而是setException()保存异常。
    5、当我们阻塞获取submit()方法结果时get(),才会将异常信息抛出。当然因为runWorker()没有抛出异常,所以并不会删除线程。
  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值