线程池执行父子任务,导致线程死锁

前言,

一次线程池的不当使用,导致了现场出现了线程死锁,接口一直不返回。而且由于这是一个公共的线程池,其他使用了次线程池的业务也一直阻塞,系统出现了OOM,不过是幸好是线程同事测试出来的,没有直接在生产坏境中出现这种事故,否则后果不堪设想。

具体情况

我接到一个需求,需要在多个excel中,根据excel中数据的关联关系,拼接出完整的记录,然后入库。其实这种情况,跟数据库中表情况挺类似的,如果将excel必做数据库中的表,就是几个表有数据,需要根据关联关系,写一个查询SQL,将查询出来的结果入库到另一张表中。

举个例子,虽然不太恰当,但是能说明情况

excel关系

学生excel、学生信息excel、详细住址excel之间的关系是一对一对一,一条学生对应一条学生信息,一条学生信息对应一条详细住址。现在就是要将这三个excel中的记录,拼接成一条完整的记录,然后入库,每个excel中都有上万条记录。这里是excel不是数据库,没法写SQL。感觉此时就像自己来实现SQL的连表逻辑,多层循环,第一层遍历学生excel的数据,拿到一条学生数据之后再遍历第学生信息excel,根据学生ID去学生信息exe找找到学生信息记录,如果还有一层关联关系,就还得套一层循环。

当然,这是最原始的想法,但是我不想这么做,一个表上万条记录,再套三层循环,效率很低了,而且就算三层循环跑完了,组装出来的上万条记录,也不可能一次性就能入库。所以我采用了线程池,我是这么想的:

  1. 那么多记录,使用线程分批处理,每个线程处理一批数据,每个批次1000条记录,相当于每次入库1000条。
  2. 当根据学生ID拿到一条学生信息记录之后,再使用线程池,分批去遍历详细住址excel,分批寻找,找到记录就起来,待所有的线程执行完成之后,将找到的记录返回去,再拼接起来,就成了完成的记录。

大概流程如下
线程池执行父子任务

task1,就是分批处理学生excel,拿到每个学生记录,再去循环学生信息excel,找到唯一的记录,进行拼接,然后再使用线程池,执行task2,根据信息ID,去分批遍历详细住址excel,找到详细住址记录,再将其拼接,最终拼接成唯一的记录,返回,入库。

原因分析

写完代码之后,我自己造了一些数据进行测试,没得问题,测试也造了一些数据测试,也没发现这个问题。(没有进行大量数据进行性能测试),丢给现场,现场同事使用真实数据进行验证的时候出了问题。为啥自测和测试同事测试都没问题,而现场同事验证就出了问题呢?本质的原因就是数据量,自测和测试同事在测试时造的数据数据量都很小,一旦数据量大了就会出现问题。

数据量小的时候,task1使用线程池中的线程,没用使用完,线程池中还有剩余的线程,所以task1执行到需要条用task2时,还有多余的线程去执行task2。而一旦数据量比较大的时候,执行到task1时,就直接将线程池中所有的线程占用完了,线程池中的所有线程都在执行task1,然后执行到需要调用task2时,又要到线程池中去获取线程,结果此时已经没有多余的线程了,task1就阻塞了,等待线程池中有空余的线程。但线程池中所有的线程都阻塞在调用task2处,都在等待,就形成了线程死锁。

解决办法

当然,出现这个问题,说明我们在设计之初就有漏洞,最正确的做法应该是设计时就不要让同一个线程池执行父子任务。那既然出现了这个问题,该如何解决呢?或者说,这个情况正确的设计是什么呢?我觉得有两个方向:

  1. 如果系统资源足够:那么就再创建一个线程池,让task2使用另一个线程池,相互独立,那么就不会出现线程死锁
  2. 如果系统资源不够:那么task2就不使用线程池进行执行,将其合入task1,。

两种方式的比较:

如果最初在设计时,我更倾向于使用方式2,因为task1将所有的线程都占满了,那说明线程池的利用率已经是最高了,将task2合入task1成一个任务,也没有什么不妥。而如果一味的去新建线程池,有滥用系统资源的嫌疑。

但是我现在的情况是,代码已经写成了这样了,我更倾向于方式1,因为那样对我原有的代码改动最小,只用将task2提交到另一个线程池就可以了,而且我们硬件资源也是足够的。如果采用方式2,改动比较大。

示例代码

这种情况是与语言无关的,我的主语言是java,所以使用java代码写一个示例,让java道友有更深刻的认识。

使用了hutool工具类和lombok,


		<!--Lombok,需装lombok插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>compile</scope>
        </dependency>

java 代码

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: liang.qin
 * @Date: 2023/2/27 19:28
 * @Email: <a href="mailto:liang.qin.work@foxmail.com">liang.qin</a>
 * @Description: 线程池执行父子任务造成死锁
 **/
public class ThreadPoolDeadlockTest1 {


    static int corePoolSize = 4;
    static int maxPoolSize = 6;
    static int keepAliveSeconds = 10;
    static int queueCapacity = 100;
    static String pollName = "excel-dispose";

    static ExecutorService executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveSeconds, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(queueCapacity),
            r -> {
                final AtomicInteger threadNumber = new AtomicInteger(1);
                Thread t = new Thread(r, pollName + "-t-" + threadNumber.getAndIncrement());
                if (t.isDaemon()) {
                    t.setDaemon(false);
                }
                if (t.getPriority() != Thread.NORM_PRIORITY) {
                    t.setPriority(Thread.NORM_PRIORITY);
                }
                return t;
            },
            new ThreadPoolExecutor.CallerRunsPolicy());


    public static void main(String[] args) {
        // 模拟学生excel
        List<Stu> stus = new ArrayList<Stu>() {{
            add(new Stu("1", "张三", 18));
            add(new Stu("2", "李四", 19));
            add(new Stu("3", "王五", 20));
            add(new Stu("4", "赵六", 21));
            add(new Stu("5", "田七", 22));
        }};
        // 模拟学生信息excel
        List<StuInfo> stuInfos = new ArrayList<StuInfo>() {{
            add(new StuInfo("1", "1", "四川xxx"));
            add(new StuInfo("2", "2", "重庆xxx"));
            add(new StuInfo("3", "3", "贵州xxx"));
            add(new StuInfo("4", "4", "云南xxx"));
            add(new StuInfo("5", "5", "甘肃xxx"));
        }};
        // 模拟详细住址excel
        List<AddInfo> addInfos = new ArrayList<AddInfo>() {{
            add(new AddInfo("1", "1", "四川"));
            add(new AddInfo("2", "2", "重庆"));
            add(new AddInfo("3", "3", "贵州"));
            add(new AddInfo("4", "4", "云南"));
            add(new AddInfo("5", "5", "甘肃"));
        }};

        disposeExcel(stus, stuInfos, addInfos);
    }


    /**
     * 处理excel
     *
     * @param stus     学生excel
     * @param stuInfos 学生信息excel
     * @param addInfos 详细地址excel
     */
    public static void disposeExcel(List<Stu> stus, List<StuInfo> stuInfos, List<AddInfo> addInfos) {

        try {
            // 多线程每批处理多少行
            int batchSize = 2;
            // 使用CompletableFuture进行编排
            int total = stus.size() % batchSize == 0 ? stus.size() / batchSize : stus.size() / batchSize + 1;
            CompletableFuture[] futures = new CompletableFuture[total];
            for (int i = 0; i < total; i++) {
                // 开始行数
                int start = batchSize * i;
                // 末尾行数
                int end = (total - 1) == i ? stus.size()-1  : start + batchSize-1;
                futures[i] = CompletableFuture.runAsync(() -> {
                    try {
                        task1(stus, stuInfos, addInfos, start, end);
                    } catch (ExecutionException | InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }, executor);
            }
            // 等待上面的线程都执行完
            CompletableFuture.allOf(futures).get();
        } catch (Exception e) {
            //    进行回滚
            System.out.println();
        }finally {
            executor.shutdown();
        }

    }


    /**
     * 任务1,使用线程池对学生excel和学生信息excel进行处理
     *
     * @param stus     学生excel
     * @param stuInfos 学生信息excel
     * @param addInfos 详细住址excel
     * @param start    需要处理的开始行
     * @param end      需要处理的末尾行
     */
    public static void task1(List<Stu> stus, List<StuInfo> stuInfos, List<AddInfo> addInfos, int start, int end) throws ExecutionException, InterruptedException {
        System.out.println(StrUtil.format("开始处理学生信息{}-{}行",start,end));
        // 循环处理学生excel
        ArrayList<Student> students = new ArrayList<>();
        for (int i = start; i <= end; i++) {
            Stu stu = stus.get(i);
            Student student = new Student();
            student.setId(IdUtil.simpleUUID());
            student.setName(stu.getName());
            student.setAge(stu.getAge());
            students.add(student);
            // 遍历学生信息excel
            for (StuInfo stuInfo : stuInfos) {
                if (stu.getId().equals(stuInfo.getStuId())) {
                    student.setAdd(stuInfo.getAdd());
                    AddInfo addInfo = task2(addInfos, stuInfo.getId());
                    student.setProvince(addInfo.getProvince());
                    break;
                }
            }
        }
        //     入库处理
        // System.out.println(StrUtil.format("完成学生信息{}-{}行入库",start,end));
        for (Student student : students) {
            System.out.println("入库数据:"+student);
        }

    }


    /**
     * 任务2,使用线程池,根据唯一的学生信息ID,去查找住址信息
     *
     * @param addInfos  住址信息excel
     * @param stuInfoId 学生信息excel
     * @return 找到的学生信息
     */
    public static AddInfo task2(List<AddInfo> addInfos, String stuInfoId) throws ExecutionException, InterruptedException {
        List<AddInfo> addInfoTemp = Collections.synchronizedList(new ArrayList<>());
        // 多线程每批处理多少行
        int batchSize = 2;
        // 使用CompletableFuture进行编排
        int total = addInfos.size() % batchSize == 0 ? addInfos.size() / batchSize : addInfos.size() / batchSize + 1;
        CompletableFuture[] futures = new CompletableFuture[total];
        for (int i = 0; i < total; i++) {
            // 开始行数
            int start = batchSize * i;
            // 末尾行数
            int end = (total - 1) == i ? addInfos.size() - 1 : start + batchSize;
            futures[i] = CompletableFuture.runAsync(() -> {
                for (int j = start; j <= end; j++) {
                    AddInfo addInfo = addInfos.get(j);
                    if (addInfo.getStuInfoId().equals(stuInfoId)) {
                        addInfoTemp.add(addInfo);
                    }
                }
            }, executor);
        }
        // 等待上面的线程都执行完
        CompletableFuture.allOf(futures).get();
        return addInfoTemp.get(0);
    }


    // 学生
    @Data
    @AllArgsConstructor
    static class Stu {
        private String id;
        private String name;
        private Integer age;
    }


    // 学习信息
    @Data
    @AllArgsConstructor
    static class StuInfo {
        private String id;
        private String stuId;
        private String add;
    }


    // 详细住址
    @Data
    @AllArgsConstructor
    static class AddInfo {
        private String id;
        private String stuInfoId;
        private String province;
    }

    // 拼接成的完整信息
    @Data
    static class Student {
        private String id;
        private String name;
        private Integer age;
        private String add;
        private String province;

        @Override
        public String toString() {
            return JSONUtil.toJsonStr(this);
        }
    }


}

运行主方法,能正常运行并且打印
正常运行的结果

但是一旦将corePoolSize这个参数调整为小于4,或者将stus这个集合中多添加两条实例数据,或者将disposeExcel方法中的batchSize参数设置成1,都会造成死锁。根本原因就是:父任务task1,将线程池中所有的线程都给占用了,子任务task2无线程可用 ,而父任务的结束有依赖于子任务的返回

补充

趁着有空,系统的学习了一下线程池相关的知识,这个现象被称为: 线程池饥饿. 这里有更加详细的视频解释和更加简单的例子: 1.线程池饥饿现象 , 2.饥饿现象解决

但是视频给出的解决方案只有我上面解决方案中的方案1

<think>嗯,用户问的是线程池中嵌套任务依赖的问题,父线程阻塞等待子线程时如何设计并发逻辑避免问题。我需要先理清楚这个场景的具体情况。 首先,线程池中的任务如果嵌套提交子任务,父任务需要等待子任务完成的话,可能会遇到什么问题呢?比如,如果线程池线程数量有限,父任务提交子任务后自己阻塞等待,这时候子任务可能因为线程池没有可用线程而无法执行导致死锁。这种情况通常被称为“线程饥饿死锁”。比如,假设线程池只有两个线程,父任务A提交子任务B,然后A等待B完成。如果A占用了第一个线程,B被提交到队列,需要第二个线程执行,但此时另一个父任务C可能占用了第二个线程,同样在等待它的子任务D,这样B和D都无法执行导致A和C都阻塞,形成死锁。 那怎么解决这个问题呢?首先可能需要考虑如何避免这种阻塞等待的情况。可能的解决方案包括调整线程池配置、改变任务提交方式、使用异步编程模型,或者使用特定的并发工具类。 比如,调整线程池的大小。如果线程池足够大,可能减少死锁的概率,但这不是根本解决办法,而且可能浪费资源。或者使用无界队列,但同样可能导致资源耗尽。 另一种思路是使用ForkJoinPool,它适合处理分治任务,工作窃取机制可以避免线程饥饿。不过ForkJoinPool通常用于可以递归分解的任务,可能需要对任务结构进行调整。 另外,使用CompletableFuture这样的异步编程工具,可以避免阻塞。父任务提交子任务后,注册回调函数,而不是阻塞等待。这样父任务可以继续执行,或者释放线程,等待子任务完成后再处理结果。但这种方法需要将代码改写为非阻塞风格,可能涉及较大的改动。 还可以考虑使用CountDownLatch或CyclicBarrier来同步父子任务,但同样需要合理设计,避免线程阻塞。比如,父任务在提交子任务后,调用latch.await(),这时候如果子任务无法执行,同样会有问题。所以可能需要结合其他方法,比如确保有足够的线程,或者将父任务的等待设置为超时模式,避免永久阻塞。 另外,任务队列的选择也可能有影响。比如使用SynchronousQueue,这样任务不会被排队,而是直接交给可用线程。但如果线程池已满,可能会导致任务被拒绝。这时候可能需要合适的拒绝策略,比如让提交任务线程自己去执行,但这样可能影响性能。 还有一种方法是避免在父任务中同步等待子任务的结果,改为异步处理。比如,将父任务拆分为多个阶段,子任务完成后触发后续操作。这需要将代码结构改为基于回调或者事件驱动的方式,可能比较复杂,但能有效避免阻塞。 总结一下,可能的解决方案包括: 1. 使用足够大的线程池,但需权衡资源。 2. 使用ForkJoinPool,利用工作窃取机制。 3. 采用异步编程模型,如CompletableFuture,避免阻塞。 4. 使用有界超时的等待机制,防止无限期阻塞。 5. 调整任务结构,避免嵌套依赖。 6. 使用特定的同步工具,并结合线程池配置优化。 需要根据具体场景选择合适的方案。比如,如果任务结构复杂,异步编程可能更合适;如果是分治任务,ForkJoinPool更好。同时,需要注意线程池的配置参数,如核心线程数、最大线程数、队列类型和拒绝策略,以避免资源耗尽和死锁。</think>在存在父子任务依赖的线程池场景中,设计并发逻辑需要遵循以下分层解决方案: ### 一、问题本质分析 $$Blocking\ Risk = f(Parent\ Blocking,\ Thread\ Starvation,\ Task\ Dependency)$$ 当父任务阻塞等待子任务时,若线程池资源耗尽,会导致死锁。典型表现为: 1. 父任务占据线程池线程 2. 子任务无法获取执行资源 3. 双方无限期等待 ### 二、技术解决方案 #### 1. 资源层优化 $$ThreadPool\ Size \geq Parent\ Tasks + Child\ Tasks$$ - **动态扩容机制**:配置可扩展线程池 ```java new ThreadPoolExecutor( corePoolSize, Integer.MAX_VALUE, // 最大线程数无上限 60L, TimeUnit.SECONDS, new SynchronousQueue<>() ); ``` - **队列选择**:使用SynchronousQueue避免任务堆积 #### 2. 结构层改造 $$Task\ Dependency\ Tree \Rightarrow Directed\ Acyclic\ Graph$$ - **Fork/Join框架**(数学表达) $$T(n) = T(n/2) + O(n) + \Theta(1)$$ ```java class RecursiveTask extends ForkJoinTask { protected void compute() { if (task.isLeaf()) { processDirectly(); } else { invokeAll(subTask1, subTask2); } } } ``` #### 3. 执行层异步化 使用$CompletableFuture$构建异步流水线: $$FutureChain = f_1.thenApply(f_2).thenCombine(f_3)$$ ```java CompletableFuture.supplyAsync(() -> childTask(), pool) .thenAcceptAsync(result -> parentCallback()); ``` #### 4. 防御层设计 $$Timeout = \max(avgTaskTime \times 3, 500ms)$$ ```java if (!latch.await(30, TimeUnit.SECONDS)) { throw new TimeoutException(); } ``` ### 三、组合策略建议 1. **黄金组合**:ForkJoinPool + CompletableFuture + 超时控制 2. **监控指标**: - $Thread\ Utilization = \frac{Busy\ Threads}{Total\ Threads}$ - $Task\ Dependency\ Depth = \max(Task\ Chain\ Length)$ 3. **熔断机制**:当检测到等待时间$W_t > threshold$时自动扩容 ### 四、数学验证模型 设线程池容量为$C$,任务依赖深度为$D$,则安全条件: $$C \geq \sum_{i=1}^{D} BranchFactor^i$$ 其中$BranchFactor$为每个任务产生的子任务数。当无法满足时,必须采用异步非阻塞方案。 通过该分层设计,可确保: $$Deadlock\ Probability \rightarrow 0\ \ \ as\ \ \ Resource\ Optimization + Async\ Flow \rightarrow \infty$$
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值