03.记一次奇妙的线程池故障

写在前面

        咱们程序员都知道,多线程是个好东西,可以异步或者并行执行任务,提高程序性能,然而多线程又好比一把双刃剑,用的好可以大幅提高程序性能,用的不好,就有可能导致程序异常,甚至崩溃。前段时间,就不慎踩坑,记录分享一下,大家一起来避坑。由于业务代码复杂,我这里按照当时业务场景的模式,写了一个可执行的demo。

出了啥问题?

        程序上线后不久,便出现假死,无法正常工作。重启后涛声依旧。

问题排查与分析

        上服务器首先topdffree三连击,结果CPU正常,磁盘正常,内存正常。并没有发现什么异常,然后通过阿里神器arthas进行分析。

arthas 分析

Arthas 用户文档 https://alibaba.github.io/arthas/

  1. 进入arthasjava -jar arthas-boot.jar
  2. 选择java pid
  3. 打开dashboard
  4. 检查线程 thread

打开arthas

进入arthas,选择java进程

image.png

dashboard

打开dashboard看看

image.png

看看线程

thread -b 看看有无死锁,并没发现死锁

[arthas@68541]$ thread -b
No most blocking thread found!
Affect(row-cnt:0) cost in 21 ms.

thread 看看线程情况,结果发现大量WAITING线程。

image.png

jstack 看看线程出现什么问题了

jstack 68541,找到了一些症状,定位到了WAITING的代码所在位置。

image.png

分析代码

找到代码位置,下面我贴出代码。

入口

package threadDeadLock;

import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @author William Zhang
 * @date 2020/7/3 3:19 下午
 * @description
 */
public class ThreadDeadLockDemo {

    /**
     * 初始化线程池
     */
    public static final ThreadPoolExecutor executor = new ThreadPoolExecutor(
            50, 100, 0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(10000), r -> new Thread(r, "zw-test-thread-pool"));

    /**
     * 程序入口
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        /**mock一定数量的任务**/
        List<Integer> skuIds = IntStream.rangeClosed(1, 100).boxed().collect(Collectors.toList());

        /**开始任务**/
        TaskAService taskAService = new TaskAService();
        taskAService.taskA(skuIds);

        /**完成后停止线程组**/
        executor.shutdown();
    }


}

ServiceA

package threadDeadLock;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

/**
 * @author William Zhang
 * @date 2020/7/3 3:49 下午
 * @description
 */
public class TaskAService {

    private TaskBService taskBService = new TaskBService();

    /**
     * taskA,对传入的用户id集合,进行并行操作。
     * @param userIds
     * @throws Exception
     */
    public void taskA(List<Integer> userIds) throws Exception{
        System.out.println("taskA begin ...");
        List<Future> futures = new ArrayList<>();
        for (Integer userId : userIds) {
            // 并行执行任务
            Future<?> future = ThreadDeadLockDemo.executor.submit(() -> {
                try {
                    taskBService.taskB(userId);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            futures.add(future);
        }
        // 等所有任务完成后返回
        for (Future future : futures) {
            future.get();
        }
        System.out.println("taskA end    ...");
    }

}

ServiceB

package threadDeadLock;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

/**
 * @author William Zhang
 * @date 2020/7/3 3:49 下午
 * @description
 */
public class TaskBService {

    /**
     * 查询商品
     */
    public Integer taskB(Integer userId) throws Exception{
        System.out.println("taskB 开始...");
        // 这里也是模拟并行查询用户明细
        List<Future> futures = new ArrayList<>();

        futures.add(queryUserInfoA(userId));
        futures.add(queryUserInfoB(userId));
        futures.add(queryUserInfoC(userId));
        futures.add(queryUserInfoD(userId));
        futures.add(queryUserInfoE(userId));
        futures.add(queryUserInfoF(userId));

        for (Future future : futures) {
            future.get();
        }
        System.out.println("taskB 结束...");
        return userId;
    }

    /*********** 下面都是模拟查询用户xxx信息的接口***********/
    private Future queryUserInfoA(Integer userId){
        Future<?> future = ThreadDeadLockDemo.executor.submit(() -> {
            try {
                // 模拟查询过程
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return future;
    }

    private Future queryUserInfoB(Integer userId){
        return queryUserInfoA(userId);
    }

    private Future queryUserInfoC(Integer userId){
        return queryUserInfoA(userId);
    }

    private Future queryUserInfoD(Integer userId){
        return queryUserInfoA(userId);
    }

    private Future queryUserInfoE(Integer userId){
        return queryUserInfoA(userId);
    }

    private Future queryUserInfoF(Integer userId){
        return queryUserInfoA(userId);
    }


}

解密

        问题非常隐晦,定位到的代码,看起来一切正常,仔细分析后发现,A方法里面有future get,然后taskA里面调用的taskB方法,也是future get,由于get会阻塞线程,那会不会是这里出现了问题呢?
        是的,线程数池的资源是宝贵的,这里形成了类似“死锁”的情景。执行taskA,它要等待taskB全部执行完成,如果此时线程池全部被taskA任务占用,taskB就进入阻塞队列,等待执行。而且阻塞队列设置的过长,无法填满,从而无法激活corePoolSize以外的线程,此时corePoolSize里面的线程没有执行完成,taskB在阻塞队列中等待,无法执行,那么也就导致taskA一直等待,这样就形成了“死锁”。

总结

        在开发时,我们要注意以下几点:

  • 设置合理的corePoolSizemaximumPoolSizequeueSizecorePoolSize太小无法利用CPU资源,太大增加线程上下文切换反而耗费资源,一般设置CPU核心数2倍。
  • queueSize同样非常重要,太大的话,其实是无法激活corePoolSize以外的线程来进行工作,具体数值按照业务来估算。
  • 线程池隔离,对于特定业务,使用专用的线程池,隔离线程之间的干扰。
  • future.get(),要设置超时时间,避免堵死整个线程。

更新后的代码

    /**
     * 初始化线程池
     */
    public static final ThreadPoolExecutor taskAExecutor = new ThreadPoolExecutor(
            5, 100, 0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(10000), r -> new Thread(r, "zw-test-taskA-thread-pool"));

    public static final ThreadPoolExecutor taskBExecutor = new ThreadPoolExecutor(
            5, 100, 0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(1000), r -> new Thread(r, "zw-test-taskB-thread-pool"));



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