【线程池】线程池什么情况下会阻塞 复盘一次线程池任务不执行问题

背景简述:某一天 产品经理反馈某个接口功能执行数据缺失,起初以为只是该接口的bug; 接着很快又反馈有另外的接口出现问题 另外这个接口是博主写的… 通过日志分析后,发现线程池里面的异步任务没有执行,两个接口都用到了异步线程 所以初步推测是线程池任务丢失。

项目定义的线程池:

        executor = ThreadPoolProxyFactory.createThreadPoolExecutor(8, 32, 20L, TimeUnit.SECONDS, new LinkedBlockingQueue(2048), (new ThreadFactoryBuilder()).setNamePrefix("common-pool-").build(), new CallerRunsPolicy());

其中拒绝策略用的是jdk自带的 (各个拒绝策略记不住具体是干嘛的不用慌,看一眼源码就知道了)

   public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

显然这个拒绝策略的作用是:当线程池满了时,用外层发起的线程去执行(比如主线程),所以任务不应该会丢失,但是大半个小时后也没见任务继续执行,我们项目数据量不太可能达到这个级别;

陷入了一个僵局:队列满了不应该会丢失任务,但任务确实没执行;如果队列没满 那么队列的任务在排队 核心线程会从队列取出来执行; 出现了这种看似悖论的问题,很大概率就是有哪个点被我们忽略了。

此外,线程池没有执行任务,说明线程出了问题没有释放,比如数据库死锁了 事务一直没提交,比如连接池耗尽;

  1. 查询数据库状态 发现最后一次死锁状态在很久之前,数据库死锁问题排除
  2. 连接池如果耗尽 那基本会有大面积报错 日志并没有看到 且数据量不太可能导致mysql连接池耗尽 也排除

现在思路继续聚焦回到线程池,原业务代码较为复杂 究极简化后发现有嵌套

tips : 线程池嵌套:
executor.execute(()->{executor.execute(()-> {
// do something
}) } )

我们知道事务嵌套会有隐患,需要熟练事务隔离问题,那线程池嵌套是不是也会有隐患呢?

我们来复习一下线程池的知识:

  1. 核心线程没满时(没达到最大核心线程),且提交线程(请求数) > 当前核心线程 , 会优先创建核心线程 直到达到最大核心线程数
  2. 提交线程大于最大核心线程数时,会往队列里面存放任务,直到队列满为止
  3. 队列满了之后,会开始创建最大线程(即非核心线程、maximumPool)
  4. 当最大线程都执行不过来时(请求数很大,核心线程、非核心线程执行不过来 队列也满了) 开始进入拒绝策略。

在我们的项目中,任务量不太可能将队列都占满,且拒绝策略为CallerRunsPolicy 主线程也没有执行,所以问题聚焦回核心线程, 说明核心线程一直没被释放。

接着就是简化所有业务代码,回归到线程池问题

tips: 生产事故难排查的原因 主要原因就是代码链路长,我们很多时候无法第一时间知道具体点在哪 每行代码都可能产生bug 需要有精准的排除不相关问题, 比如我们最开始怀疑就是事务问题,因为事务造成死锁 尤其在多线程环境中 是非常常见的,但是查询数据库状态并没有发现死锁,且mysql死锁一般日志也会有deadlock提示, 所以排除了是数据库的问题,才将思路转回线程池本身。

在将业务代码剥离后,N个方法调用链路平铺开来, 得到以下代码:

(这里解释一下,博主是已经先有了思路 带着答案写简化demo来验证的)

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8, 32, 20L, TimeUnit.SECONDS, new LinkedBlockingQueue(2048));
        // 模拟接收到一堆通知事件 
        for (int i = 0; i < 15; i++) {
            AtomicInteger atomicInteger = new AtomicInteger(i);
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("进入外层:" + atomicInteger.get());
                    // 模拟上传三方接口 	
                    List<CompletableFuture<Pair>> uploadContractFeatures = new ArrayList<>(3);

                    for (int j = 0; j < 3; j++) {
                        int i1 = atomicInteger.get();
                        CompletableFuture<Pair> suppliedAsync = CompletableFuture.supplyAsync(
                                () -> {
                                    System.out.println("进入内层:" + i1);
                                    return new Pair((long) i1, Boolean.TRUE);
                                },
                                threadPoolExecutor
                        )
                                .exceptionally(ex -> {
                                    System.out.println("======异常");
                                    new Pair<Long, Boolean>(Long.valueOf(i1), Boolean.FALSE);
                                    return new Pair(Long.valueOf(i1), Boolean.FALSE);
                                });

                        uploadContractFeatures.add(suppliedAsync);
                    }
                    // 等待上传任务完成
                    CompletableFuture.allOf(uploadContractFeatures.toArray(new CompletableFuture[0])).join();
                    System.out.println("---------------------执行完毕");
                }
            });
        }

可以得出结论:

  1. 外层会先占用核心线程,此时内层已经拿不到核心线程了
  2. 任务数远没达到最大队列,所以非核心线程也没有创建,仍依赖核心线程执行
  3. 内层拿不到核心线程 且CompletableFuture.allOf 在阻塞线程(等待内层线程执行完毕)
  4. 结果就是 外层在等待内层完成,内层拿不到线程 一直在阻塞,内层没完成,外层也无法释放,导致了整个线程池不可用

题外话:因为核心线程阻塞 任务会一直往队列中添加,如果用的是无界队列 在时间过久后 将发生OOM (虽然在还没到这个时间点 也早就应该发现线程池出问题了 但确实是一个OOM可能产生的点)

我们也可以通过jconsole来辅助分析:
在这里插入图片描述

针对项目更合理的解决方案:
内层任务更换其它线程池,例如:


ThreadPoolExecutor executorService = new ThreadPoolExecutor(
		4,
		32,
		10L,
		TimeUnit.SECONDS,
		new SynchronousQueue<>(),
		new ThreadFactoryBuilder().setNamePrefix("inner-pool-").build(),
		new ThreadPoolExecutor.CallerRunsPolicy()
	);

核心线程不会过大 避免资源浪费,最大线程32 (项目够用),
队列使用SynchronousQueue 表示零容量队列强制线程池优先创建新线程(而非缓冲任务)
CallerRunsPolicy 拒绝策略表示线程满了后 用当前线程执行 保证任务不丢失

写在最后:适合自己业务的技术才是好技术,不浪费资源,且资源够用才是恰到好处;例如数据量更大的情况下,就不能使用SynchronousQueue了

阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池阻塞线程池
<think>嗯,用户问的是异步执行任务需要线程池用什么阻塞队列。这个问题看起来是关于Java线程池配置的,尤其是阻塞队列的选择。那我得先回忆一下线程池的相关知识。 首先,线程池的核心参数包括核心线程数、最大线程数、阻塞队列以及拒绝策略。当任务提交到线程池时,流程通常是这样的:如果当前线程数小于核心线程数,就创建新线程处理任务;如果核心线程已满,任务会被放入阻塞队列;当队列满了,才会创建新线程直到达到最大线程数;如果都满了,就执行拒绝策略。 阻塞队列在这里的作用是缓冲任务同的队列类型有同的特性,影响线程池的行为。用户想知道在异步任务情况下应该选哪种队列。可能需要分析同队列的特点,以及它们适用场景。 常见的阻塞队列有几种: 1. **ArrayBlockingQueue**:基于数组的有界队列,固定大小。如果队列满了,会根据策略处理任务。适合需要控制资源使用的场景,防止内存溢出。但可能因为队列容量限制导致任务被拒绝或线程数增加。 2. **LinkedBlockingQueue**:基于链表的队列,默认无界,但可以设置容量。无界的话,任务会被拒绝,但可能导致内存问题。而有界时,表现类似ArrayBlockingQueue。Executors.newFixedThreadPool默认使用这个队列,可能适合任务量波动较大的情况。 3. **SynchronousQueue**:存储元素的队列,每个插入操作必须等待另一个线程的移除操作。所以提交任务时,如果没有可用线程,会直接创建新线程直到达到最大线程数,之后触发拒绝策略。Executors.newCachedThreadPool用它,适合任务处理快且数量大的情况。 4. **PriorityBlockingQueue**:支持优先级的无界队列,任务按优先级执行。适用于需要按优先级处理任务的场景,但同样有无界队列的内存风险。 5. **DelayedWorkQueue**(在ScheduledThreadPoolExecutor中使用):用于延迟任务或周期性任务,按执行时间排序。 那么,针对异步执行任务,选择哪种队列合适呢?需要看具体场景: - 如果任务可预测,可能突然激增,使用无界队列(如LinkedBlockingQueue)可能导致内存问题,所以需要根据系统承受能力选择有界队列,并配合合适的拒绝策略。 - 如果希望快速响应任务,并且允许创建更多线程,可能用SynchronousQueue,这样在没有空闲线程时会立即创建新线程,直到达到最大线程数。但要注意最大线程数的设置,否则可能创建过多线程影响性能。 - 对于需要控制资源的情况,比如稳定的系统负载,用ArrayBlockingQueue可以限制队列大小,避免资源耗尽,但需要合理设置队列容量和最大线程数。 此外,线程池任务类型也很重要。如果是CPU密集型,线程数宜过多,可能队列容量可以设大一些;如果是IO密集型,线程数可以调大,队列容量可能需要太大,或者配合其他队列类型。 还要考虑拒绝策略,比如当队列满时,是直接丢弃任务、抛出异常,还是其他处理方式。同的队列选择会影响拒绝策略触发的频率。 总结来说,没有绝对正确的答案,需要根据任务特性、系统资源、性能要求来决定。可能需要给出同队列的适用场景,让用户根据实际情况选择。同时提醒合理配置线程数和队列容量,以及配合拒绝策略,才能达到最佳效果。</think>在异步任务执行场景中,线程池选择阻塞队列时,需要根据任务特性、资源限制和性能需求综合判断。以下是常见阻塞队列的对比及适用场景: --- ### **1. SynchronousQueue(零容量队列)** **特性**: - 队列存储任务,插入操作必须等待移除操作 - 直接传递任务给线程,无缓冲能力 **适用场景**: - 需要`立即创建新线程`处理任务的场景 - 任务处理速度快,希望`最大化线程利用率` - 典型应用:`Executors.newCachedThreadPool`默认使用此队列 **示例配置**: ```java new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60s, new SynchronousQueue<>()); ``` --- ### **2. LinkedBlockingQueue(无界/有界队列)** **特性**: - 基于链表的队列,默认`无界`(容量=Integer.MAX_VALUE) - 可设置固定容量变为有界队列 **适用场景**: - 需要`缓冲大量任务`且允许短时内存增长的场景 - 任务`到达速率波动大`,需要削峰填谷 - 典型应用:`Executors.newFixedThreadPool`默认使用无界队列 **风险提示**: - 无界队列可能导致`OOM`(当任务持续积压时) - 建议生产环境`显式设置合理容量` --- ### **3. ArrayBlockingQueue(固定容量队列)** **特性**: - 基于数组的`有界队列`,容量固定 - 采用公平锁策略可避免线程饥饿 **适用场景**: - 需要`严格控制内存消耗`的场景 - 要求`稳定的系统负载`(防止任务突增) - 典型应用:`资源受限系统`的核心服务线程池 **示例配置**: ```java new ThreadPoolExecutor(coreSize, maxSize, 30s, new ArrayBlockingQueue<>(1000)); ``` --- ### **4. PriorityBlockingQueue(优先级队列)** **特性**: - 支持`按优先级排序任务`的无界队列 - 必须实现`Comparable`接口或提供Comparator **适用场景**: - 需要`优先处理高优先级任务`的异步场景 - 典型应用:VIP用户请求优先处理、紧急告警优先响应 --- ### **选择策略对比表** | 队列类型 | 容量 | 内存风险 | 吞吐量 | 适用场景 | |---------------------|-------|--------|------|---------------------------| | SynchronousQueue | 0 | 低 | 高 | 短任务、高响应要求 | | LinkedBlockingQueue | 无界/有界 | 高/中 | 中 | 任务量波动大、允许缓冲 | | ArrayBlockingQueue | 有界 | 低 | 高 | 稳定负载、资源受限系统 | | PriorityBlockingQueue | 无界 | 高 | 中 | 需要任务优先级调度 | --- ### **最佳实践建议** 1. **CPU密集型任务**: - 推荐`ArrayBlockingQueue` + 较小队列容量(如CPU核数*2) - 配合`ThreadPoolExecutor.CallerRunsPolicy`拒绝策略,避免过度排队 2. **IO密集型任务**: - 优先选择`LinkedBlockingQueue`(设置合理容量) - 增大核心线程数,利用队列缓冲等待IO资源释放 3. **混合型任务**: - 使用`PriorityBlockingQueue`区分CPU/IO任务优先级 - 高优先级任务设置短超时,低优先级任务允许排队 4. **关键服务保障**: - 对队列实现`监控埋点`,当队列深度超过阈值时触发告警 - 使用`自定义拒绝策略`记录任务信息或降级处理 ```java // 典型混合配置示例 ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, // 核心线程数=CPU核数 16, // 最大线程数=4*4 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200), // 根据内存设置合理容量 new CustomRejectedExecutionHandler() // 自定义拒绝策略 ); ``` --- ### **注意事项** - 所有无界队列都需要配合`合理的拒绝策略`,防止OOM - 使用`SynchronousQueue`时需设置`合理的最大线程数`,避免创建过多线程 - 对队列深度、活跃线程数等指标进行监控,动态调整队列策略 通过合理选择阻塞队列,可以使线程池在吞吐量、响应速度和资源消耗之间达到最佳平衡。实际选择时建议通过压力测试验证同队列的表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟秋与你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值