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

最近接了一个业务需求,需求倒是不难,三下五除二就整理出设计方案,然后就开始代码改造。

啪,很快,就完成代码改造,然后提测给测试小姐姐。

小姐姐前面测试好好的,测到这个工程的时候,突然跟我反馈,你看这个这个工程跑着跑着就不动了,日志什么也没了。

那时候正在忙,想着我就没改几行代码,也没涉及核心逻辑,那肯定没问题的。

于是回复小姐姐,业务逻辑执行的太慢了吧,再等个半小时再看看?

一小时后,小姐姐又来找我,我都等了一小时,这个工程还是没动啊,日志还是没有啊。

这下不能拖了,上去仔细一看,还真是,怎么就没了呢?

先简单说下这段代码,就是使用一个异步线程执行一段业务逻辑,示例代码如下:

// 前置逻辑

.....
Thread thread=new Thread(new Runnable() {
    @Override
    public void run() {
        try {
           // 异步线程执行其他业务逻辑
        } catch (Exception e) {
           // 不进行任何代码处理
        }
    }
});
thread.start();

凭着老程序员的经验,猜到可能是异步线程内发生了异常,导致异步线程退出,不再继续执行。而又因为上述代码「吃掉」了异常,这就导致我们从外部看起来这个工程跑着跑着就不动了,日志什么也没了。

于是改造了一下,打印出相关异常日志,最终定位问题,原来是小姐姐造的数据存在问题,从而引发 NPE 问题。

「不知道大家有没有碰到过上面的情况,使用线程异步执行相关逻辑,但是执行到一半突然就像卡主一般,不再继续往下执行。」

小黑哥碰到过几次,这几次原因都不太相同,总结起来分为下面三种情况:

异步任务长时间被阻塞

异步任务发生异常

异步任务异常被吃掉

异步任务长时间被阻塞
第一种,异步线程执行任务,这个任务需要通过网络调用其他远端服务。假设服务端响应的非常慢,而我们设置的网络超时时间又很长,这就会导致这个线程长时间被阻塞。

假设异步任务伪码如下:

ThreadPoolExecutor threadPool= ....;
threadPool.execute(() -> {
// 1.调用远端服务
Socket socket....;
// 2.设置超时时间
socket.setSoTimeout(60*1000);
// 3.读取服务端返回
socket.read();
});

上面程序中,如果服务端一直没有返回,那么异步线程将会一直被阻塞,直到超时。

这种情况其实还好,我们无非等待一段时间,就可以看到异步线程继续往下执行任务。

举一个极端的例子,假设上面的代码没有设置超时时间,而服务端一直没有返回响应,「此时异步线程就会被一直阻塞」。

除了上面网络读取阻塞的例子,常见情况还有

执行了长时间休眠,比如 TimeUnit.MINUTES.sleep(60)
内部发生了死锁
等等
如果异步线程长时间被阻塞,而异步任务执行又比较频繁,那么线程池内可用线程将会被慢慢耗尽,此时后续任务就会被拒绝执行。

解决办法

其实非常简单,首先我们使用 jstack 命令 「dump」 一下当前 Java 应用的线程堆栈情况,然后根据线程池名字定位相关线程即可。

在这里插入图片描述

网上随便找了堆栈图

如果没有自定义线程池 ThreadFactory 参数,那查找定位被阻塞线程就比较麻烦了。
所以创建线程池建议自定义 ThreadFactory 参数,这对于后期排查问题非常有用。

异步任务异常未捕获
上面的情况,异步线程其实还活着,只是被阻塞没办法执行后续的逻辑。

那这一类情况呢,与上面不太一样,由于异步任务内部发生错误,抛出异常,而代码逻辑中又没有进行捕获处理,从而导致线程提前异常退出。

异常退出伪码如下:

// 1.创建执行的任务
Runnable runnable=new Runnable() {
    @Override
    public void run() {
       // 执行前置逻辑
        // 抛出异常
        int i=100/0;
       // 执行后置逻辑
        
    }
};
// 2.创建线程
Thread thread=new Thread(runnable);
// 3.运行异步线程
thread.start();
// 其他业务逻辑

上述代码中,异步线程执行到除零逻辑,将会抛出异常,然后异步线程将会异常退出。

「异步线程内抛出的异常日志仅仅只会被打印到控制台,而不会被记录到日志文件中。」

所以正常的业务日志中是见不到线程异常的日志,这就给了我们一种假象,异步线程看起来还在执行任务,其实它已经挂了。

PS:上面的话可能不好理解,举个例子,如果你使用 IDEA 执行上面这段程序,异常日志将会被输出到 IDEA 下方控制台。 而如果我们在Linux 机器上执行这段程序,异常日志仅仅只会显示在当前终端窗口上,一旦关闭当前终端窗口,日志就没。了。 如果想要保存这种日志,我们需要将stdout 重定向到日志文件中,比如执行以下命令:

-- 将 stdout 重定向输出到文件中 
nohup java  xxxx > $STDOUT_FILE 2>&1 &

解决办法

第一种解决办法,其实很多读者已经想到了,异步线程内使用 try..catch 语句捕获所有异常即可。

「没错,就是这么简单。」

不过这里提一点,一般我们使用 try..catch仅仅只会捕获 Exception异常。

那么极端情况下,异步线程内如果抛出 Error,比如抛出了 java.lang.NoClassDefFoundError,此时是没法捕获,异步线程依旧会异常退出。

所以我们可以使用try..catch捕获 Throwable,这样及时发生 Error错误,也会被捕获。

不过个人觉得捕获Exception异常就够了,正常工程应用很少会发生 Error错误,所以我们只要了解有这个可能即可。

> ps:之前同事上线一个应用,使用异步线程执行任务,每次执行到一半,都不再继续执行。 由于异步线程内使用try..catch捕获处理了
> Exception异常,所以找了半天不知道什么问题。 最后,小黑哥排查 stdout 输出日志,才发现异步线程发生 Error错误。
> 这种解决本法需要我们主动去捕获异常,而下面第二种解决办法,设置线程异常处理方法。

一旦设置完成,如果异步线程内发生异常,线程退出之前将会调用异常处理方法。

我们拿 Thread 来讲,其设置方法如下:

```bash
Runnable runnable=new Runnable() {
    @Override
    public void run() {
        int i=100/0;
    }
};

Thread thread=new Thread(runnable);
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t.getName()+"发生异常"+e.getMessage());
    }
});

thread.start();
不过生产环境不建议直接使用 Thread,我们需要使用线程池代替。

线程池设置异常处理方法可以分为两种,如果我们使用 ThreadPoolExecutor#execute执行异步任务,那我们需要在自定义线程池的时候,使用 ThreadFactory 设置。

ThreadPoolExecutor threadPool =new ThreadPoolExecutor(
        5,
        10,
        60,
        TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),
      // 这里使用 Guava 的 ThreadFactoryBuilder 类,方便构造 ThreadFactory
        new ThreadFactoryBuilder().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                // 处理异常
            }
        }).build()
        );

如果你当前使用 ThreadPoolExecutor#submit执行异步任务,那就简单了,我们可以直接通过 Future#get获取到线程内抛出的异常。

Future<?> future = threadPool.submit(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        return "小黑十一点半";
    }
});

try {
    future.get();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    // 线程内抛出异常将会被封装在 ExecutionException 内
}

异步任务异常被吃掉
好了,终于到最后一种情况了,小黑哥这次碰到就是这种😭。

这种情况具体来说就是异步线程内使用 try…catch 语句捕获了所有异常,但是没有在 catch语句中进行任何代码处理。

Thread thread=new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            int i=100/0;
        } catch (Exception e) {
           // 不进行任何代码处理
        }
    }
});
thread.start();

如上述代码所示,catch语句中没有进行任何代码处理。即使异步线程内真发生了异常,也不会有任何提示,这个异常就像被吃掉一般。

总结
多线程编程原本就比较复杂,我们需要处理各种问题,那今天主要介绍了一下其中的一个问题:

「异步线程突然停止,就像卡主一般,不再继续执行代码逻辑,没有任何响应」

那这类问题,小黑哥根据自己碰到情况,总结为三类:

异步任务长时间被阻塞

异步任务异常

异步任务异常被吃掉。

对于第一种,我们在网络编程中及时设置超时时间,一般都能避免。

对于第二、第三种情况,这就需要我们建立一个良好的编程习惯,使用try…catch 捕获所有异常,并且 catch块中一定做一些处理,比如说打印相关日志。

以下文章来源于小黑十一点半 ,作者楼下小黑哥
文章链接:https://mp.weixin.qq.com/s/NgiuxCXISsWM7vTy6iUjDw

总结了2020面试题,这份面试题的包含的模块分为19个模块,分别是:
 Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、
 Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、
 MySQL、Redis、JVM 。

获取资料以上资料:关注公众号:有故事的程序员,获取学习资料。
记得点个关注+评论哦~

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 有以下几种可能性: 1. 所有线程都已经完成了它们的任务,因此全部变成了就绪状态。 2. 线程之间存在竞争条件或死锁,导致所有线程无法继续运行,只能等待资源的释放或锁的解除,进入就绪状态。 3. 操作系统的调度算法导致所有线程都被分配到了相同的时间片,因此它们在同一时间内全部处于就绪状态。 无论是哪种情况,都需要通过排查和分析来找出问题的原因,并采取相应的措施来解决。 ### 回答2: 线程突然全部就绪状态的原因可能有几种情况: 首先,当线程的任务执行完毕或者等待的条件满足时,线程会进入就绪状态。例如,线程执行的任务是一个循环,当循环结束时,线程就会进入就绪状态,等待下一次调度执行。 其次,线程调度器的调度算法也可能导致线程突然全部就绪状态。线程调度器会根据一定的策略来决定将哪些线程放入就绪队列中。当某个调度时间片段结束时,调度器可能会将当前运行的线程切换出来,然后将其他线程放入就绪队列中,造成线程突然全部就绪状态。 此外,线程之间的竞争也可能导致线程全部就绪状态。当多个线程竞争同一资源时,如果某个线程获取到了资源,其他线程就会进入就绪状态,等待资源被释放。当资源被释放后,这些线程就会同时进入就绪状态。 需要注意的是,线程着突然全部就绪状态并不一定是一种正常的情况,可能存在某种异常情况或者错误导致的。在实际开发过程中,我们需要对线程的状态进行合理的监控和管理,以确保线程的正常执行和协调。 ### 回答3: 线程是计算机程序执行的最小单位,一个程序可以包含多个线程线程的状态包括就绪、运行、阻塞和终止。 当线程着突然全部就绪状态了,可能有以下几种情况: 1. 线程任务执行完毕:线程可能在执行某个任务时,所需的代码已经全部执行完毕,线程没有其他任务可执行,因此进入了就绪状态。 2. CPU资源调度问题:线程的调度是由操作系统的内核进行管理的,如果操作系统的调度算法将所有线程的优先级相同或相近,那么在特定的调度周期内,所有线程都有机会获得CPU资源,导致全部线程进入就绪状态。 3. 线程等待资源释放:线程执行过程中可能会请求一些共享资源,如果这些资源被其他线程占用且未释放,导致线程无法继续执行。但当资源释放后,所有等待此资源的线程会同时进入就绪状态。 需要注意的是,线程进入就绪状态并不意味着立即执行线程调度是由操作系统决定的,线程只有在获得CPU资源后才能运行。因此,即使线程全部就绪,也可能会有一部分线程执行,而其他线程仍然处于就绪状态。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值