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

问题描述

线上环境异步任务全部未执行,代码没有抛出任何异常和提示,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()发生了阻塞。

  1. 也可以使用 Arthas定位


    image.png
image.png
状态 场景 原因
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方法。

问题分析

线程池内部结构


image.png

当线程1中的任务A嵌套了任务C后,任务C被放到了阻塞队列,这时线程1就被柱塞了,必须等到任务C执行完毕。这时如果其他线程也发生相同清空,如线程2的任务B,他的嵌套任务D也被放入阻塞队列,这是线程2也会被阻塞。如果这类任务比较多时就会将所有线程池的线程阻塞住。最后导致线程池假死,所有异步任务无法执行。

解决办法

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

思考

我们程序代码使用的@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的设计这为什么要这样设计,这里有这么明显的问题,难道他们不知道吗,我理解这样设计的初衷可能就是为了避免上诉我们发现的任务嵌套问题,因为每个任务单独线程执行是不会发生上诉程序假死的情况的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容