当前位置: 首页 > 科技观察

有些线程跑着跑着就不见了

时间:2023-03-16 22:29:33 科技观察

一些线程在运行时消失了。转载本文请联系小黑11:30公众号。前言大家好,我是楼下的小黑哥~最近接到一个业务需求,需求不难。三遍五遍整理了设计方案,然后开始代码改造。啪,很快,代码修改完成,然后提交给测试小姐姐。小姐姐过去测试的不错。她在测试这个项目的时候,突然给了我反馈。可以看到这个项目运行后就不动了,日志里什么也没有。当时比较忙,心想没改几行代码,没涉及到核心逻辑,肯定没问题。于是回复小姐姐,业务逻辑执行太慢了,再等半小时再看?一个小时后,小姐姐又来找我了,我等了一个小时,项目还是没动,log还是No,现在拖不动了。上去仔细看看。这是真的。为什么不见了?首先简单说一下这段代码,就是用一个异步线程来执行一段业务逻辑。示例代码如下://前置逻辑.....Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){try{//异步线程执行其他业务逻辑}catch(Exceptione){//否代码处理}}});thread.start();根据老程序员的经验,我猜测可能是异步线程发生了异常,导致异步线程退出,不再继续执行。又因为上面的代码“吃”了异常,这导致我们从外面看运行后项目没有动,日志里什么也没有。于是自己修改,打印出相关的异常日志,终于定位到问题所在。原来是小姐姐创建的数据有问题,导致了NPE问题。“不知道大家有没有遇到过上面的情况,用线程异步执行相关逻辑,但是执行到一半突然像个卡主,不继续执行了。”小黑哥遇到过好几次了。两次的原因不尽相同,归纳为以下三种情况:异步任务长时间阻塞。异步任务异常。异步任务异常被吃掉。异步任务长时间阻塞。通过网络调用其他远程服务。假设服务器响应很慢,我们设置的网络超时时间很长,会导致线程长时间阻塞。假设异步任务的伪代码如下:ThreadPoolExecutorthreadPool=...;threadPool.execute(()->{//1.调用远程服务Socketsocket....;//2.设置超时套接字。setSoTimeout(60*1000);//3.读取服务器返回socket.read();});在上面的程序中,如果服务器还没有返回,异步线程就会被阻塞,直到超时。这种情况其实还好,我们稍等片刻,就可以看到异步线程还在继续执行任务。举个极端的例子,假设上面的代码没有设置超时时间,服务端还没有返回响应,“此时异步线程将永远阻塞”。除了上面网络读阻塞的例子,常见的情况是执行了长时间的睡眠,比如TimeUnit.MINUTES.sleep(60)内部死锁等。如果异步线程长时间阻塞,异步线程任务执行的越频繁,那么线程池中的可用线程就会慢慢耗尽,后续的任务就会拒绝执行。解决方法其实很简单。首先,我们使用jstack命令“dump”查看当前Java应用程序的线程栈,然后根据线程池名称定位到相关线程。网上随便找的栈图。如果没有自定义线程池ThreadFactory参数,查找定位阻塞线程会比较麻烦。因此,建议在创建线程池时自定义ThreadFactory参数,这对后期排查问题非常有用。异步任务异常并没有捕捉到上述情况。异步线程其实还活着,只是被阻塞了,无法执行后续逻辑。这种情况与上述不同。因为异步任务内部发生错误,抛出异常,代码逻辑没有捕捉到,导致线程提前异常退出。异常退出的伪代码如下://1.创建并执行任务Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){//执行前置逻辑//抛出异常inti=100/0;//执行后置逻辑}};//2.创建一个线程Threadthread=newThread(runnable);//3.运行异步线程thread.start();//其他业务逻辑上面代码中,异步线程执行除零逻辑,会抛出异常。那么异步线程就会异常退出。“异步线程中抛出的异常日志只会打印到控制台,不会记录到日志文件中。”所以正常的业务日志是看不到线程异常日志的,这给我们一种错觉,异步线程看似还在执行任务,但实际上已经挂掉了。PS:上面的话可能不太好理解。比如你用IDEA执行上面的程序,异常日志会输出到IDEA下的控制台。而如果我们在Linux机器上执行这个程序,异常日志只会显示在当前终端窗口。一旦当前终端窗口关闭,日志就会消失。向上。如果我们要保存这种日志,我们需要将stdout重定向到日志文件,例如执行如下命令:--Redirecttheoutputofstdouttothefilenohupjavaxxxx>$STDOUT_FILE2>&1&Solution第一种方案,其实,有很多读者已经想到了,只要在异步线程中使用try..catch语句,就可以捕获所有的异常。“是的,就这么简单。”但是这里有一点,一般我们使用try..catch只是为了捕获Exception异常。极端情况下,如果异步线程抛出Error,比如java.lang.NoClassDefFoundError,此时无法捕获,异步线程仍会异常退出。所以我们可以使用try..catch来捕获Throwable,这样Error错误就会被及时捕获。不过我个人觉得,捕获Exception就够了。Error错误在正常的工程应用中很少出现,所以我们只需要了解可能性即可。ps:之前有个同事启动了一个应用,使用异步线程执行任务。每次执行到一半,他就停止执行。由于在异步线程中使用了try..catch来捕获并处理Exception异常,找了半天也不知道是什么问题。最后小黑哥查看了stdout输出日志,才发现异步线程发生了Error。这种方案需要我们主动去捕捉异常,下面第二种方案是设置线程异常处理方式。一旦设置,如果异步线程内发生异常,异常处理方法将在线程退出前被调用。我们以Thread为例,设置方法如下:{@OverridepublicvoiduncaughtException(Threadt,Throwablee){System.out.println(t.getName()+"发生异常"+e.getMessage());}});thread.start();但是在生产环境中不建议直接使用Thread,我们需要使用线程池来代替。线程池设置异常处理的方法可以分为两种。如果我们使用ThreadPoolExecutor#execute来执行异步任务,那么在自定义线程池的时候就需要使用ThreadFactory设置。ThreadPoolExecutorthreadPool=newThreadPoolExecutor(5,10,60,TimeUnit.SECONDS,newArrayBlockingQueue<>(100),//这里使用Guava的ThreadFactoryBuilder类,方便构造ThreadFactorynewThreadFactoryBuilder().setUncaughtExceptionHandler(newThread.UncaughtExceptionHandler(){@OverridepublicvoiduncaughtException(Threadt,Throwablee){//处理异常}}).build());如果你当前使用ThreadPoolExecutor#submit来执行异步任务,很简单,我们直接通过Future#get获取线程中抛出的异常即可。Futurefuture=threadPool.submit(newCallable(){@OverridepublicObjectcall()throwsException{return"小黑十一点半";}});try{future.get();}catch(InterruptedExceptione){e.printStackTrace();}catch(ExecutionExceptione){//线程中抛出的异常会封装在ExecutionException}异步任务异常已经吃完了,终于上次的情况了,小黑哥这次是这样的时候我遇到了。具体来说,这种情况是所有的异常都被异步线程中的try..catch语句捕获,但是catch语句中没有进行任何代码处理。Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){try{inti=100/0;}catch(Exceptione){//不进行任何代码处理}}});thread.start();如上代码所示,catch语句中没有进行任何代码处理。即使异步线程真的发生了异常,也不会提示,异常就像被吃掉一样。综上所述,多线程编程本来就是复杂的,我们需要处理各种各样的问题。今天主要介绍其中一个问题:“异步线程突然停止,就像卡主一样,没有任何响应就没有继续执行代码逻辑”。根据小黑遇到的情况,他可以将这类问题归纳为三类:异步任务长时间阻塞。异步任务异常被吃掉。对于第一种,我们在网络编程中及时设置了超时时间,一般是可以避免的。对于第二种和第三种情况,我们需要养成良好的编程习惯,使用try..catch捕获所有异常,并在catch块中做一些处理,比如打印相关日志。