多线程下嵌套异步任务导致程序假死问题

 更新时间:2024年08月20日 10:51:28   作者:xiaolyuh123  
这篇文章主要介绍了多线程下嵌套异步任务导致程序假死问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

问题描述

线上环境异步任务全部未执行,代码没有抛出任何异常和提示,CPU、内存都很正常,基本没有波动,GC也没啥异常的。

问题原因

经定位是异步由于嵌套异步任务使用了Future.get()方法导致的程序阻塞

手动使用线程池示例

public class FutureBlockTest {
    public static void main(String[] args) {
        // 为了模拟我这里只存创建一个工作线程
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
        // 第一层异步任务
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "-main-thread");
            // 第二层异步任务(嵌套任务)
            FutureTask<Long> futureTask = new FutureTask<>(() -> {
                System.out.println(Thread.currentThread().getName() + "-child-thread");
                return 10L;
            });
            fixedThreadPool.execute(futureTask);
            System.out.println("子任务提交完毕");

            // 获取子线程的返回值
            try {
                System.out.println(futureTask.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        };
        // 提交主线
        fixedThreadPool.submit(runnable);
    }
}

执行上诉示例后输出

pool-1-thread-1-main-thread
子任务提交完毕

然后程序假死。

使用@Async示例

// 程序入口
@Controller
public class AsyncController {
    @Autowired
    private MainThreadService mainThreadService;

    @GetMapping("/")
    public String helloWorld() throws Exception {
        mainThreadService.asyncMethod();
        return "Hello World";
    }
}

// 主任务代码
@Service
public class MainThreadService {
    @Autowired
    private ChildThreadService childThreadService;

    @Async("asyncThreadPool")
    public void asyncMethod() throws Exception {
        // 主任务开始
        // TODO
        // 开启子任务
        Future<Long> longFuture = childThreadService.asyncMethod();
        // 子任务阻塞子任务
        longFuture.get();
        // TODO
    }
}
// 子任务示例
@Service
public class ChildThreadService {
    @Async("asyncThreadPool")
    public Future<Long> asyncMethod() throws Exception {
        // 子任务执行
        Thread.sleep(1000);
        // 返回异步结果
        return new AsyncResult<>(10L);
    }
}

定位

1.通过jpsjstack命令定位

jstack 81173 | grep 'WAITING' -A 15

admin@wangyuhao spring-boot-student % jstack 81173 | grep 'WAITING' -A 15
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076b541b38> (a java.util.concurrent.FutureTask)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
        at java.util.concurrent.FutureTask.get(FutureTask.java:191)
        at com.xiaolyuh.FutureBlockTest.lambda$main$1(FutureBlockTest.java:28)
        at com.xiaolyuh.FutureBlockTest$$Lambda$1/885951223.run(Unknown Source)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
        at java.util.concurrent.FutureTask.run(FutureTask.java)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

可以定位到是futureTask.get()发生了阻塞。

2.也可以使用 Arthas定位

状态场景原因
BLOCKED线程处于BLOCKED状态的场景1.当前线程在等待一个monitor lock,比如synchronizedhuo或者Lock。
WAITING线程处于WAITING状态的场景1. 调用Object对象的wait方法,但没有指定超时值。
2. 调用Thread对象的join方法,但没有指定超时值。
3. 调用LockSupport对象的park方法。
TIMED_WAITING线程处于TIMED_WAITING状态的场景1. 调用Thread.sleep方法。
2. 调用Object对象的wait方法,指定超时值。
3. 调用Thread对象的join方法,指定超时值。
4. 调用LockSupport对象的parkNanos方法。
5. 调用LockSupport对象的parkUntil方法。

问题分析

线程池内部结构

当线程1中的任务A嵌套了任务C后,任务C被放到了阻塞队列,这时线程1就被柱塞了,必须等到任务C执行完毕。

这时如果其他线程也发生相同清空,如线程2的任务B,他的嵌套任务D也被放入阻塞队列,这是线程2也会被阻塞。

如果这类任务比较多时就会将所有线程池的线程阻塞住。最后导致线程池假死,所有异步任务无法执行。

解决办法

  • futureTask.get()必须加上超时时间,这样至少不会导致程序一直假死
  • 不要使用嵌套的异步任务,或者嵌套任务不要获取子任务结果,不要阻塞主任务
  • 将主任务和子任务的线程池拆分成两个线程池池,不要使用同一个线程池(推荐)

思考

我们程序代码使用的@Async注解,也就是示例二的代码。使用注解默认配置,那么Spring会给所有任务分配单独线程,且线程不能重用,源码如下:

获取Executor源码

org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor

	/**
	 * This implementation searches for a unique {@link org.springframework.core.task.TaskExecutor}
	 * bean in the context, or for an {@link Executor} bean named "taskExecutor" otherwise.
	 * If neither of the two is resolvable (e.g. if no {@code BeanFactory} was configured at all),
	 * this implementation falls back to a newly created {@link SimpleAsyncTaskExecutor} instance
	 * for local use if no default could be found.
	 * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME
	 */
	@Override
	protected Executor getDefaultExecutor(BeanFactory beanFactory) {
		Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
		return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
	}

获取执行任务源码

org.springframework.core.task.SimpleAsyncTaskExecutor#doExecute

	/**
	 * Template method for the actual execution of a task.
	 * <p>The default implementation creates a new Thread and starts it.
	 * @param task the Runnable to execute
	 * @see #setThreadFactory
	 * @see #createThread
	 * @see java.lang.Thread#start()
	 */
	protected void doExecute(Runnable task) {
		Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
		thread.start();
	}

我们可以发现默认执行@Async注解的异步线程池,内部其实就没用线程池,它会给每一个任务创建一个新的线程,线程使用过后会销毁掉,线程不会重用。

  • 那它将会带来一个问题,那就是异步任务过多就会不断创建线程,最终将系统资源耗尽。
  • 这也是网络上大部分文章不推荐直接使用@Async注解默认配置的原因。

我们需要思考的是,Spring的设计这为什么要这样设计,这里有这么明显的问题,难道他们不知道吗,我理解这样设计的初衷可能就是为了避免上诉我们发现的任务嵌套问题,因为每个任务单独线程执行是不会发生上诉程序假死的情况的。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java实现动态验证码生成

    Java实现动态验证码生成

    这篇文章主要为大家详细介绍了Java实现动态验证码生成,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • spring基础系列之JavaConfig配置详解

    spring基础系列之JavaConfig配置详解

    本篇文章主要介绍了spring基础系列之JavaConfig配置详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • spring mvc高级技术实例详解

    spring mvc高级技术实例详解

    前面学习了简单的Spring Web知识,接着学习更高阶的Web技术。下面这篇文章主要给大家介绍了spring mvc高级技术的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起看看吧
    2018-09-09
  • Mybatis-Plus中IdType.AUTO局部配置不生效的问题解决

    Mybatis-Plus中IdType.AUTO局部配置不生效的问题解决

    本文主要介绍了Mybatis-Plus中IdType.AUTO局部配置不生效的问题解决,数据库插入数据时,id的默认生成方式还是雪花算法,局部配置没有生效,下面就来解决一下,感兴趣的可以了解一下
    2023-09-09
  • SpringBoot项目配置文件注释乱码的问题解决方案

    SpringBoot项目配置文件注释乱码的问题解决方案

    这篇文章主要介绍了SpringBoot 项目配置文件注释乱码的问题解决方案,文中通过图文结合的方式给大家讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-07-07
  • linux部署出现java文件操作报错:java.io.FileNotFoundException解决办法

    linux部署出现java文件操作报错:java.io.FileNotFoundException解决办法

    这篇文章主要g介绍了linux部署出现java文件操作报错:java.io.FileNotFoundException解决的相关资料,这个错误通常表示你的Spring Boot应用程序无法找到指定的文本文件,需要的朋友可以参考下
    2023-12-12
  • IntelliJ IDEA 2021.1 推出语音、视频功能,边写代码边聊天(功能超级强大)

    IntelliJ IDEA 2021.1 推出语音、视频功能,边写代码边聊天(功能超级强大

    这篇文章主要介绍了IntelliJ IDEA 2021.1 推出语音、视频功能,边写代码边聊天(功能超级强大),本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • WebClient抛UnsupportedMediaTypeException异常解决

    WebClient抛UnsupportedMediaTypeException异常解决

    这篇文章主要为大家介绍了WebClient抛UnsupportedMediaTypeException异常的解决方案,文中给大家介绍了六中方案,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2022-02-02
  • java结束当前循环常用代码

    java结束当前循环常用代码

    在 Java中,当我们要结束一个循环时,通常会使用循环变量的实现类来结束,但在实际开发中,我们经常会遇到某个循环结束后需要进行其他的操作的情况,在本文中给大家分享java结束当前循环常用代码,感兴趣的朋友跟随小编一起看看吧
    2023-06-06
  • Eclipse在线安装hibernate插件

    Eclipse在线安装hibernate插件

    这篇文章主要介绍了Eclipse在线安装hibernate插件,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-04-04

最新评论