Java 线程池

线程遇到未处理的异常就结束了

这个好理解,当线程出现未捕获异常的时候就执行不下去了,留给它的就是垃圾回收了。

线程池中线程频繁出现未捕获异常

当线程池中线程频繁出现未捕获的异常,那线程的复用率就大大降低了,需要不断地创建新线程。
做个实验:

  1. public class ThreadExecutor {
  2. private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
  3. new ArrayBlockingQueue<>(200), new ThreadFactoryBuilder().setNameFormat("customThread %d").build());
  4. @Test
  5. public void test() {
  6. IntStream.rangeClosed(1, 5).forEach(i -> {
  7. try {
  8. Thread.sleep(100);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. threadPoolExecutor.execute(() -> {
  13. int j = 1/0;
  14. });});
  15. }
  16. }

新建一个只有一个线程的线程池,每隔0.1s提交一个任务,任务中是一个1/0的计算。

  1. Exception in thread "customThread 0" java.lang.ArithmeticException: / by zero
  2. at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
  3. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  4. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  5. at java.lang.Thread.run(Thread.java:748)
  6. Exception in thread "customThread 1" java.lang.ArithmeticException: / by zero
  7. at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
  8. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  9. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  10. at java.lang.Thread.run(Thread.java:748)
  11. Exception in thread "customThread 2" java.lang.ArithmeticException: / by zero
  12. at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
  13. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  14. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  15. at java.lang.Thread.run(Thread.java:748)
  16. Exception in thread "customThread 3" java.lang.ArithmeticException: / by zero
  17. at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
  18. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  19. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  20. at java.lang.Thread.run(Thread.java:748)
  21. Exception in thread "customThread 4" java.lang.ArithmeticException: / by zero
  22. at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
  23. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  24. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  25. at java.lang.Thread.run(Thread.java:748)
  26. Exception in thread "customThread 5" java.lang.ArithmeticException: / by zero
  27. at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
  28. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  29. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  30. at java.lang.Thread.run(Thread.java:748)

可见每次执行的线程都不一样,之前的线程都没有复用。原因是因为出现了未捕获的异常。
把异常捕获试试:

  1. public class ThreadExecutor {
  2. private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
  3. new ArrayBlockingQueue<>(200), new ThreadFactoryBuilder().setNameFormat("customThread %d").build());
  4. @Test
  5. public void test() {
  6. IntStream.rangeClosed(1, 5).forEach(i -> {
  7. try {
  8. Thread.sleep(100);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. threadPoolExecutor.execute(() -> {
  13. try {
  14. int j = 1 / 0;
  15. } catch (Exception e) {
  16. System.out.println(Thread.currentThread().getName() +" "+ e.getMessage());
  17. }
  18. });
  19. });
  20. }
  21. }
  1. customThread 0 / by zero
  2. customThread 0 / by zero
  3. customThread 0 / by zero
  4. customThread 0 / by zero
  5. customThread 0 / by zero

可见当异常捕获了,线程就可以复用了。

问题来了,代码中异常不可能全部捕获

如果要捕获那些没被业务代码捕获的异常,可以设置Thread类的uncaughtExceptionHandler属性。这时使用ThreadFactoryBuilder会比较方便,ThreadFactoryBuilder是guava提供的ThreadFactory生成器。

  1. new ThreadFactoryBuilder()
  2. .setNameFormat("customThread %d")
  3. .setUncaughtExceptionHandler((t, e) -> System.out.println(t.getName() + "发生异常" + e.getCause()))
  4. .build()

修改之后:

  1. public class ThreadExecutor {
  2. private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
  3. new ArrayBlockingQueue<>(200),
  4. new ThreadFactoryBuilder()
  5. .setNameFormat("customThread %d")
  6. .setUncaughtExceptionHandler((t, e) -> System.out.println("UncaughtExceptionHandler捕获到:" + t.getName() + "发生异常" + e.getMessage()))
  7. .build());
  8. @Test
  9. public void test() {
  10. IntStream.rangeClosed(1, 5).forEach(i -> {
  11. try {
  12. Thread.sleep(100);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. threadPoolExecutor.execute(() -> {
  17. System.out.println("线程" + Thread.currentThread().getName() + "执行");
  18. int j = 1 / 0;
  19. });
  20. });
  21. }
  22. }
  1. 线程customThread 0执行
  2. UncaughtExceptionHandler捕获到:customThread 0发生异常/ by zero
  3. 线程customThread 1执行
  4. UncaughtExceptionHandler捕获到:customThread 1发生异常/ by zero
  5. 线程customThread 2执行
  6. UncaughtExceptionHandler捕获到:customThread 2发生异常/ by zero
  7. 线程customThread 3执行
  8. UncaughtExceptionHandler捕获到:customThread 3发生异常/ by zero
  9. 线程customThread 4执行
  10. UncaughtExceptionHandler捕获到:customThread 4发生异常/ by zero

可见,结果并不是想象的那样,线程池中原有的线程没有复用!所以通过UncaughtExceptionHandler想将异常吞掉使线程复用这招貌似行不通。它只是做了一层异常的保底处理。
excute改成submit试试

  1. public class ThreadExecutor {
  2. private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
  3. new ArrayBlockingQueue<>(200),
  4. new ThreadFactoryBuilder()
  5. .setNameFormat("customThread %d")
  6. .setUncaughtExceptionHandler((t, e) -> System.out.println("UncaughtExceptionHandler捕获到:" + t.getName() + "发生异常" + e.getMessage()))
  7. .build());
  8. @Test
  9. public void test() {
  10. IntStream.rangeClosed(1, 5).forEach(i -> {
  11. try {
  12. Thread.sleep(100);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. Future<?> future = threadPoolExecutor.submit(() -> {
  17. System.out.println("线程" + Thread.currentThread().getName() + "执行");
  18. int j = 1 / 0;
  19. });
  20. try {
  21. future.get();
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. } catch (ExecutionException e) {
  25. e.printStackTrace();
  26. }
  27. });
  28. }
  29. }
  1. 线程customThread 0执行
  2. java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
  3. 线程customThread 0执行
  4. java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
  5. 线程customThread 0执行
  6. java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
  7. 线程customThread 0执行
  8. java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
  9. 线程customThread 0执行
  10. java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero

通过submit提交线程可以屏蔽线程中产生的异常,达到线程复用。当get()执行结果时异常才会抛出。
原因是通过submit提交的线程,当发生异常时,会将异常保存,待future.get();时才会抛出。
这是Futuretask的部分run()方法,看setException

  1. public void run() {
  2. try {
  3. Callable<V> c = callable;
  4. if (c != null && state == NEW) {
  5. V result;
  6. boolean ran;
  7. try {
  8. result = c.call();
  9. ran = true;
  10. } catch (Throwable ex) {
  11. result = null;
  12. ran = false;
  13. setException(ex);
  14. }
  15. if (ran)
  16. set(result);
  17. }
  18. }
  19. }
  20. protected void setException(Throwable t) {
  21. if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
  22. outcome = t;
  23. UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
  24. finishCompletion();
  25. }
  26. }

将异常存在outcome对象中,没有抛出,再看get方法:

  1. public V get() throws InterruptedException, ExecutionException {
  2. int s = state;
  3. if (s <= COMPLETING)
  4. s = awaitDone(false, 0L);
  5. return report(s);
  6. }
  7. private V report(int s) throws ExecutionException {
  8. Object x = outcome;
  9. if (s == NORMAL)
  10. return (V)x;
  11. if (s >= CANCELLED)
  12. throw new CancellationException();
  13. throw new ExecutionException((Throwable)x);
  14. }

当outcome是异常时才抛出。

总结

1、线程池中线程中异常尽量手动捕获
2、通过设置ThreadFactoryUncaughtExceptionHandler可以对未捕获的异常做保底处理,通过execute提交任务,线程依然会中断,而通过submit提交任务,可以获取线程执行结果,线程异常会在get执行结果时抛出。