关于异步线程池使用以及线程饥饿的思考

1. 线程饥饿的定义与本质

定义:
线程饥饿指的是在多线程并发执行时,由于某些线程长时间无法获得执行所需的 CPU 资源或系统资源,从而导致这些线程的任务一直处于等待状态,严重时可能使这些线程永远得不到执行。

本质:

  • 资源竞争:多个线程争夺有限的 CPU 时间片、锁或 I/O 资源,部分线程因为优先级低或调度策略原因,无法获得执行机会。
  • 任务提交过快:在异步任务提交中,如果使用了单一线程池且任务提交速度远大于线程执行速度,可能会迅速将线程池队列填满,进而触发线程池的拒绝策略。
  • 拒绝策略问题:例如,当采用 CallerRunsPolicy 时,被拒绝的任务会由调用线程执行,从而形成单线程串行执行,导致整个系统吞吐量大幅下降。

二、案例分析

//定义一个线程池
    @Bean(name = "taskWorkExecutor")
    public Executor taskWorkExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setKeepAliveSeconds(300);
        executor.initialize();
        return executor;
    }

public void doSomeThing(){

        CompletableFuture.runAsync(() -> {

             //查询需要处理的任务

            ArrayList<Object> taskList= Lists.newArrayList();

            taskList.stream().map(item -> CompletableFuture.runAsync(() -> {
                //业务流程

            }, taskWorkExecutor));

        }, taskWorkExecutor);

}

存在的问题

  1. 任务提交数量膨胀
    doSomeThing() 方法中,主线程提交的任务直接由同一个线程池来处理。如果 taskList 中的元素非常多,会迅速占满线程池的队列。当队列满时,CallerRunsPolicy 将使拒绝的任务由提交任务的线程执行,这将导致原本的并发任务被串行化执行,严重降低吞吐量。

  2. 线程池资源竞争
    如果所有任务都使用同一个线程池,不同类型的任务(例如 CPU 密集型和 I/O 密集型任务)会互相竞争资源,可能导致部分高优先级任务无法及时执行,从而出现线程饥饿问题

三、解决思路

3.1 优化线程池管理

方案一:拆分线程池

  • 高 CPU 占用任务池:将 CPU 密集型任务单独分配到一个线程池中,设置较低的核心线程数和较小的队列容量,以防止大量任务同时占用 CPU 资源。
  • 低 CPU 占用任务池:将 I/O 密集型或短任务统一交由另一个线程池处理,该线程池可以配置较高的核心线程数和较大队列,保证响应速度。
  • 任务分发池:建立一个中间层线程池用于任务的初步调度,将接收到的大量任务按类别或优先级分发到不同的线程池,避免单一线程池被任务淹没。

方案二:使用 CompletableFuture 任务编排
使用 CompletableFuture 可以将各个任务的执行链解耦,根据不同的任务类型,选择对应的线程池执行,达到线程池分层设计,并以流式方式组织任务顺序。例如:

private void taskProcess(){

// 定义IO密集型线程池和计算密集型线程池
ExecutorService ioExecutor = Executors.newCachedThreadPool();
ExecutorService computeExecutor = Executors.newFixedThreadPool(4);

CompletableFuture.supplyAsync(() -> fetchFromDatabase(), ioExecutor) // 使用IO线程池
    .thenApplyAsync(data -> processData(data), computeExecutor)       // 切换至计算线程池
    .thenAccept(result -> saveResult(result));                       // 使用默认线程池(可选)

}

方案三:任务超时控制
为了防止线程长时间等待而导致资源无法释放,可以在提交任务时设置超时时间:

public vodi taskProcess(){
//指定异步线程的超时时间,防止线程假死
CompletableFuture.runAsync(()->{}).orTimeout(5, TimeUnit.MINUTES);
}

4. 提交任务的注意事项

  • 拒绝策略问题
    在使用 CallerRunsPolicy 时,主线程可能会被迫执行任务,导致任务串行化。为此,可以考虑:

    • 采用更合理的线程池配置,调整核心线程数和队列容量;
    • 对不同任务设置不同的线程池,避免混用同一线程池导致的互相干扰。
  • 监控和日志
    建议在提交任务和执行任务时增加详细日志记录,监控线程池的运行状态和队列长度,及时发现并调整性能瓶颈。

  • 任务拆分和批处理
    如果任务数量非常大,可以考虑对任务进行分批提交,或采用分页加载的方式,避免一次性提交过多任务导致线程池队列快速饱和。

5. 总结

问题归纳:

  • 线程饥饿:当任务提交数量过多,单一线程池容易被快速填满队列,导致部分任务得不到及时执行。
  • 拒绝策略导致串行化:使用 CallerRunsPolicy 时,被拒绝的任务在主线程中执行,可能破坏原本的并发设计,降低吞吐量。
  • 线程池资源竞争:不同类型的任务竞争同一个线程池资源,会导致部分高优先级任务无法及时获得 CPU 时间片。

解决方案:

  1. 拆分线程池:根据任务类型分别建立不同的线程池,避免资源互相争用。
  2. 利用 CompletableFuture 解耦任务链:使任务编排更优雅,明确任务先后顺序。
  3. 设置任务超时:避免任务长时间阻塞,保证系统响应及时。
  4. 优化拒绝策略与任务提交策略:根据业务场景调整线程池参数,并通过分批提交任务减少瞬时压力。
  5. 增加监控和日志:在关键位置添加监控代码,及时捕获并处理线程饥饿问题,通过配置文件的方式配置线程池核心参数,对于线程池核心参数扩容的情况,可以进行动态增加(动态减少的话支持不太够,不符合预期)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值