查看原文
其他

如何优雅的关闭Java线程池

清泉 IT轻社区 2019-01-12

⾯试中经常会问到,创建⼀个线程池需要哪些参数、线程池的工作原理,却很少会问到线程池如何安全关闭的。


也正是因为⼤家不是很关注这块,即便是⼯作三四年的⼈,也会有因为线程池关闭不合理,导致应用⽆法正常stop的情况,还有出现⼀些报错的问题。


本篇就以ThreadPoolExecutor为例例,来介绍下如何优雅的关闭线程池。


线程中断

在介绍线程池关闭之前,先介绍下Thread的interrupt。


在程序中,我们是不能随便中断⼀个线程的,因为这是极其不安全的操作,我们⽆法知道这个线程正运⾏在什么状态,它可能持有某把锁,强⾏中断可能导致锁不能释放的问题;或者线程可能在操作数据库,强⾏中断导致数据不一致,从而混乱的问题。正因此,Java⾥将Thread的stop⽅法设置为过时,以禁⽌⼤家使⽤。


⼀个线程什么时候可以退出呢?当然只有线程自⼰才能知道。


所以我们这⾥要说的Thread的interrrupt⽅法,本质不是⽤来中断一个线程。而是将线程设置⼀个中断状态。当我们调⽤线程的interrupt方法,它有两个作⽤用:


1、如果此线程处于阻塞状态(比如调⽤了wait方法,io等待),则会立刻退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做⼀定的处理,然后让线程退出。

2、如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用isInterrupted方法来查看自⼰是否被中断,并做退出操作。



注:如果线程的interrupt方法先被调用,然后线程调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。如果线程捕获InterruptedException异常后,继续调用阻塞方法, 将不再触发InterruptedException异常。




线程池的关闭

线程池提供了两个关闭方法,shutdownNow和shuwdown⽅法。


shutdownNow⽅法的解释是:线程池拒接收新提交的任务,同时⽴刻关闭线程池,线程池里的任务不再执行。


shutdown⽅法的解释是:线程池拒接收新提交的任务,同时等待线程池⾥的任务执行完毕后关闭线程池。


以上的说法虽然没错,但是还有很多的细节问题,比如调用shutdown⽅法后,正在执⾏的任务的线程会做出什么反应?正在等待任务的线程又会做出什么反应?线程在什么情况下才会彻底退出。如果不了解这些细节,在关闭线程池时就难免遇到,像线程池关闭不了,关闭线程池出现报错等情况。


再说这些关闭线程池细节问题之前,需要强调一点的是,调用完shutdownNow和shuwdown⽅法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调⽤awaitTermination⽅法进⾏同步等待。


有了以上介绍,下⾯就结合线程池源码,分别说说这两个线程池关闭方法的⼀一些实现细节。


shutdownNow


我们看⼀下shutdownNow⽅法的源码:



在shutdownNow⽅法里,重要的三句代码我⽤红⾊数字标出来了。第⼀句就是原⼦性的修改线程池的状态为STOP状态(比较简单,我就不贴代码了) ,第三句是将队列里还没有执⾏的任务放到列表里,返回给调用方,第⼆句是遍历线程池⾥的所有⼯作线程,然后调用线程的interrupt方法。如下图:



以上就是shutdownNow⽅法的执⾏逻辑:将线程池状态修改为STOP,然后调⽤线程池⾥的所有线程的interrupt⽅法。


调⽤shutdownNow后,线程池⾥的线程会做如何反应呢?那就要看,线程池⾥的线程正在执⾏的代码逻辑了。其在线程池的runWorker⽅法里(对线程池的执行原理不了解的,请看之前的文章),其代码如下:



正常情况下,线程池里的线程,就是在这个while循环里不停地执行。其中代码task.run()就是在执⾏我们提交给线程池的任务,如当我们调用shutdownNow  时,task.run()⾥⾯正处于IO阻塞,则会导致报错,如果task.run()⾥正在正常执

⾏,则不受影响,继续执⾏完这个任务。


从上图看得出来,如果getTask()⽅法返回null,也会导致线程的退出。我们再来看看getTask⽅法的实现:

如果我们调⽤shutdownNow⽅法时,线程处于从队列⾥读取任务⽽阻塞中(图中下边的红框),则会导致抛出InterruptedException异常,但因为异常被捕获,线程将会继续在这个for循环⾥执⾏。


还记得shutdownNow⽅法里将线程修改为STOP状态吧,当执行到上边红框⾥的代码时,由于STOP状态值是⼤于SHUTDOWN状态,STOP也大于等于STOP,不管任务队列是否为空,都会进⼊if语句从而返回null,线程退出。



总结当我们调用线程池的shutdownNow时,如果线程正在getTask方法中执⾏,则会通过for循环进入到if语句,于是getTask 返回null,从而线程退出。不管线程池⾥是否有未完成的任务。如果线程因为执行提交到线程池里的任务而处于阻塞状态,则会导致报错。(如果任务里没有捕获InterruptedException异常),否则线程会执行完当前任务,然后通过getTask方法返回为null来退出。



shutdown


我们再来看看shutdown⽅法的源码:



跟shutdownNow类似,只不过它是将线程池的状态修改为SHUTDOWN状态,然后调⽤interruptIdleWorkers方法,来中断空闲的线程。这是interruptIdleWorkers⽅法的实现:



跟shutdownNow⽅法调⽤interruptWorkers⽅法不同的是,interruptIdleWorkers⽅法在遍历线程池⾥的线程时,有一个w.tryLock()加锁判断,只有加锁成功的线程才会被调用interrupt方法。那么什么情况下才能被加锁成功?什么情况下不能被加锁成功呢?这就需要我们继续回到线程执行的runWorker方法。


在上边runWorker方法代码的截图中,我刻意将w.lock()和w.unlock()调用用红框 圈起。其实就是正运行在w.lock和w.unlock之间的线程将因为加锁失败,而不会被调用interrupt方法,换句话说,就是正在执行线程池里任务的线程不会被中断。


不管是被调用了interrupt的线程还是没被调用的线程,什么时候退出呢?,这就要看getTask⽅法的返回是否为null了。


在getTask里的if判断(上文中getTask代码截图中上边红色方框的代码)中,由于线程池被shutdown⽅法修改为SHUTDOWN状态,SHUTDOWN大于等于

SHUTDOWN成⽴没问题,但是SHUTDOWN不⼤于等于STOP状态,所以只有队列为空,getTask方法才会返回null,导致线程退出。



总结当我们调用线程池的shuwdown方法时,如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,⽽是继续执行。如果线程池阻塞等待从队列⾥读取任务,则会被唤醒,但是会继续判断队列是否为空,若不为空,则会继续从队列里读取任务,若为空则线程退出。


优雅的关闭线程池


有了上边对两个关闭线程池方法的了解,相信优雅安全关闭线程池将不再是问题。


我们知道,使⽤shutdownNow⽅法,可能会引起报错,使用shutdown方法可能会导致线程关闭不了。


所以当我们使⽤shutdownNow⽅法关闭线程池时,一定要对任务里进行异常捕获。


当我们使用shuwdown方法关闭线程池时,一定要确保任务里不会有永久阻塞等待的逻辑,否则线程池就关闭不了。


最后,⼀定要记得shutdownNow和shuwdown调用完,线程池并不是⽴刻就关闭了,要想等待线程池关闭,还需调用awaitTermination⽅法来阻塞等待。


END



往期作者热门文章


注:作者赞赏码,请大家支持鼓励!

作者:清泉

介绍:多年从事互联网开发,以Java后端开发为主,其它为辅。先后在某上市游戏公司做页游服务端开发,也曾在创业公司做移动APP后端架构设计,目前在阿里巴巴从事资深研发的工作。

点击“阅读原文”,加入作者知识星球深度交流!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存