线程池 多线程运行结束后 如何关闭? ExecutorService的正确关闭方法

前言

最近在使用ExecutorService的时候,对于与ExecutorService相关的概念有些迷糊,

加上本身ExecutorService内部的有些方法名在取名上也容易让使用者误解,导致

犯了一些错误。在解决的过程中,偶尔看到了日本人写的一篇文章简单明了,通俗易懂

所以想着翻译成中文希望能够帮助到与我有一样困惑的程序员朋友们。

原文地址如下:

http://gurimmer.lolipop.jp/daihakken/2012/01/27/javaexecutorserviceの正しい終了shutdownの仕方/

闲话少说,文章如下


虽然使用ExecutorService可以让线程处理变的很简单,
可是有没有人觉得在结束线程运行时候只调用shutdown方法就可以了?
实际上,只调用shutdown方法的是不够的。

我们用学校的老师和学生的关系来说明这个问题。

 

shutdown只是起到通知的作用

 

我们来假设如下场景:
学校里在课上老师出了一些问题安排全班同学进行解答并对学生说“开问题解答完毕后请举手示意!”
如果有学生解答完毕后会举手对老师说“老师我做完了!”,如果大家都解题完毕后上课结束。

上面的场景对应于ExecutorService里的方法的话是下面的样子。
老师: ExecutorService
学生: ExecutorService里的线程
问题: 通过参数传递给ExecutorService.execute的任务(Runnable)
授课: main线程
学校: Java进程

 

“问题解答完毕后请举手示意!”是shutdown方法。“老师我做完了!”是各个任务(Runnable)的运行结束。
所有的任务(Runnable)都结束了的话main线程(授课)也结束了。

在这里,我们假设试卷中有难度较大的问题,当然学生解答较难的问题也会比较花时间。
在上面的场景中老师除了shutdown方法之外什么也做不了,只能呆呆得等着学生们说,“老师我做完了!”之后才可以有下一步动作。
这都是因为shutdown方法只是用来通知的方法。

这时如果即使授课时间结束(main线程结束),学校也不能放学(Java进程结束),因为学生们还在解题中呢。这个时候如果你是老师你会怎么做?

一般的情况肯定是经过一定的时间在授课快要结束的时候,如果还有人没有解答出来的话,或者公布给大家解题方法,

或者作为课后习题让学生回去继续思考,然后结束上课对不对!

 

定好下课时间后等待结束

 

如果经过了一定的时间任务(Runnable)还不结束的时候我们可以通过中止任务(Runnable)的执行,以防止一直等待任务的结束。
awaitTermination方法正是可以实现这个中止作用的角色。

具体的使用方法是,在shutdown方法调用后,接着调用awaitTermination方法。这时只需要等待awaitTermination方法里第一个参数指定的时间。
如果在指定的时间内所有的任务都结束的时候,返回true,反之返回false。返回false意味着课程结束的时候还有题目没有解答出来的学生。

通过shutdownNow方法,我们可以作为老师向同学发出“没有解答出来的同学明天给出解答”的命令后结束授课。

shutdownNow方法的作用是向所有执行中的线程发出interrupted以中止线程的运行。这时,各个线程会抛出InterruptedException异常(前提是
线程中运行了sleep等会抛出异常的方法)

所以正确的中止线程的方法如下:

[java]  view plain  copy
  1. public static void main(String[] args) {  
  2.    
  3.     ExecutorService pool = Executors.newFixedThreadPool(5);  
  4.     final long waitTime = 8 * 1000;  
  5.     final long awaitTime = 5 * 1000;  
  6.    
  7.     Runnable task1 = new Runnable(){  
  8.         public void run(){  
  9.             try {  
  10.                 System.out.println("task1 start");  
  11.                 Thread.sleep(waitTime);  
  12.                 System.out.println("task1 end");  
  13.             } catch (InterruptedException e) {  
  14.                 System.out.println("task1 interrupted: " + e);  
  15.             }  
  16.         }  
  17.     };  
  18.    
  19.     Runnable task2 = new Runnable(){  
  20.         public void run(){  
  21.             try {  
  22.                 System.out.println("  task2 start");  
  23.                 Thread.sleep(1000);  
  24.                 System.out.println("  task2 end");  
  25.             } catch (InterruptedException e) {  
  26.                 System.out.println("task2 interrupted: " + e);  
  27.             }  
  28.         }  
  29.     };  
  30.     // 让学生解答某个很难的问题  
  31.     pool.execute(task1);  
  32.    
  33.     // 生学生解答很多问题  
  34.     for(int i=0; i<1000; ++i){  
  35.         pool.execute(task2);  
  36.     }  
  37.    
  38.     try {  
  39.         // 向学生传达“问题解答完毕后请举手示意!”  
  40.         pool.shutdown();  
  41.    
  42.         // 向学生传达“XX分之内解答不完的问题全部带回去作为课后作业!”后老师等待学生答题  
  43.         // (所有的任务都结束的时候,返回TRUE)  
  44.         if(!pool.awaitTermination(awaitTime, TimeUnit.MILLISECONDS)){  
  45.             // 超时的时候向线程池中所有的线程发出中断(interrupted)。  
  46.             pool.shutdownNow();  
  47.         }  
  48.     } catch (InterruptedException e) {  
  49.         // awaitTermination方法被中断的时候也中止线程池中全部的线程的执行。  
  50.         System.out.println("awaitTermination interrupted: " + e);  
  51.         pool.shutdownNow();  
  52.     }  
  53.    
  54.     System.out.println("end");  
  55. }  


 

可以看出上面程序中waitTime的值比awaitTime大的情况下,发生Timeout然后执行中的线程会中止执行而结束。
反过来如果缩小waitTime的值,增大awaitTime的值的的话,各个线程就会不被中止的正常运行至结束。

在这里,如果我们把awaitTime和shutdownNow方法全部屏蔽掉的只留下shutdown方法的话会怎样呢?

会变成表示main方法结束的「end」显示出来之后,会打印出很多的task2的start和end。
这就是虽然课程结束了,但是学校仍然不能放学的不正常状态。最恶劣的情况会导致JAVA进程一直残留在OS中。

所以我们一定不要忘记使用awaitTermination和shutdownNow

 

shutdown也是很重要的

看了上面的描述后可能有些人会认为,只需要执行awaitTermination和shutdownNow就可以正常结束线程池中的线程了。其实不然。
shutdown方法还有「大家只解答我要求的问题,其它的不用多做」的意思在里面。

shutdown方法调用后,就不能再继续使用ExecutorService来追加新的任务了,如果继续调用execute方法执行新的任务的话

就会抛出RejectedExecutionException异常。(submit方法也会抛出上述异常)

而且,awaitTermination方法也不是在它被调用的时间点上简单得等待任务结束而是在awaitTermination方法调用后,

持续监视各个任务的状态以或者是否线程已经运行结束。所以不调用shutdown方法执行调用awaitTermination的话由于追加出来的任务可能

会导致任务状态监视出现偏差而发生预料之外的awaitTermination的Timeout异常

 

正确的调用顺序是

shutdown方法
awaitTermination方法
shutdownNow方法(发生异常或者是Timeout的时候)

 

实际开发的系统可能会有不能强制线程中止执行的场景出现,所以虽然推荐使用上面说的调用顺序但也并不是绝对一成不变的。

另外,可以经过一定时间间隔而有计划调用任务执行的ScheduledExecutorService同样适用于上面说的调用顺序,但是在使用scheduled方法的时候需要另外一些步骤。

### Java 线程池启动和安全关闭 #### 启动线程池 为了高效管理线程并处理大量并发任务,推荐使用 `java.util.concurrent.Executors` 工具类来创建预配置好的线程池实例。对于只需要一个工作线程的应用场景,可以采用如下方式初始化单一线程池: ```java ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); ``` 这种方式能够确保所有提交的任务按照顺序在一个单独的工作线程上执行[^1]。 当需要更灵活控制核心/最大线程数、队列长度等参数时,则应考虑直接利用 `ThreadPoolExecutor` 构造函数自定义线程池对象;但是要注意避免每次请求到来时都新建线程池的做法,这会带来不必要的开销并且可能导致资源耗尽问题[^2]。 #### 安全关闭线程池的最佳实践 在线程池不再被需要的情况下,应当采取适当措施释放其所占用的资源。通常有两种方法用于优雅地终止线程池内的活动: - **shutdown()**: 发送信号给线程池表示它应该停止接受新的任务,并等待已提交的任务完成(包括正在运行中的)。调用此方法之后不能再向该线程池提交新任务。 - **shutdownNow()**: 尝试立即停止所有的活跃任务,并返回尚未开始执行的任务列表。请注意这个操作可能会导致某些未保存的数据丢失或事务中断等问题,在实际应用中需谨慎评估其影响后再决定是否使用。 下面给出一段示范代码展示如何正确地开启与结束一个固定大小为5的核心线程数量级的缓存型线程池: ```java import java.util.concurrent.*; public class ThreadPoolExample { private static final ExecutorService threadPool; static { int corePoolSize = 5; int maxPoolSize = 10; long keepAliveTimeInSeconds = 60L; BlockingQueue<Runnable> workQueue = new LinkedBlockingDeque<>(10); threadPool = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTimeInSeconds, TimeUnit.SECONDS, workQueue, new ThreadPoolExecutor.CallerRunsPolicy() ); } public void submitTask(Runnable task){ try{ threadPool.submit(task); } catch (RejectedExecutionException e){ System.err.println("Task submission failed due to pool shutdown or rejection policy."); } } public void gracefulShutdown(){ threadPool.shutdown(); // Initiate orderly shutdown. try { if (!threadPool.awaitTermination(800, TimeUnit.MILLISECONDS)){ threadPool.shutdownNow(); // Forceful termination after timeout. } } catch (InterruptedException ie) { threadPool.shutdownNow(); Thread.currentThread().interrupt(); // Preserve interrupt status. } } } ``` 这段代码展示了如何构建一个具有特定配置选项的线程池,并提供了两种关闭策略——一种是在合理时间内尝试平滑退出(`shutdown()` 和 `awaitTermination`),另一种则是强制性的快速清理 (`shutdownNow()`)。此外还加入了对拒绝服务异常的支持以应对可能发生的过载状况。 值得注意的是,在多线程环境中处理异常非常重要。由于子线程抛出的异常不会自动传播到主线程,因此应在每个独立任务内部实现合适的错误捕捉机制而不是依赖外部包裹式的 `try...catch` 结构[^3]。 最后提醒一点,针对不同类型的服务需求设计专门定制化的线程池有助于提高整体系统的响应速度和服务质量。例如,I/O 密集型任务适合较大的线程数目而 CPU 密集型则相反[^4]。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值