浅谈踩坑记之一个Java线程池参数,差点引起线上事故(下)

简介: 浅谈踩坑记之一个Java线程池参数,差点引起线上事故

三、 关于线程池配置

线程池配置非常重要,但是往往很容易忽视,配置不合理或者线程池复用次数少,依然会频繁的创建和销户。

1.如何合理计算核心线程数?

我们可以通过接口平均响应时间和服务需要支撑的QPS计算 例如: 我们接口平均RT 0.005s,那么,一个工作线程可以处理任务数200 如果单机需要支撑QPS 3W,那么可以计算出 需要核心线程数 150

即公式: QPS ➗ (1 ➗ 平均RT) = QPS * RT

2.容易忽视的 @Async 注解

Spring中使用 @Async 注解 默认线程池是 SimpleAsyncTaskExecutor,默认情况下如果没有配置等于没有使用线程池,因为它每次都会重新创建一个新的线程,不会复用。

所以切记,如果使用@Async 一定要配置.


@EnableAsync
@Configuration
@Slf4j
public class ThreadPoolConfig {
    private static final int corePoolSize = 100;             // 核心线程数(默认线程数)
    private static final int maxPoolSize = 400;             // 最大线程数
    private static final int keepAliveTime = 60;            // 允许线程空闲时间(单位:默认为秒)
    private static final int queueCapacity = 0;         // 缓冲队列数
    private static final String threadNamePrefix = "Async-Service-"; // 线程池名前缀
    @Bean("taskExecutor") 
    public ThreadPoolTaskExecutor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

四、 线程池飙升如何引起的?

Dubbo服务端工作线程我们配置如下:

corethreads: 150
threads: 800
threadpool: cached
queues: 10

看上去是不是挺合理的,设置很小队列数,是为了防止抖动引起短暂线程池不足情况。从上面看,貌似也没什么问题,从白天业务量来说核心线程数是完全够用的(RT<5ms, QPS<1w)。可是上线之后,线程池一路飙升,最大达到阈值最大值800, 报警信息如下:

org.apache.dubbo.remoting.RemotingException("Server side(IP,20880) thread pool is exhausted, detail msg:Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-IP:20880, Pool Size: 800 (active: 4, core: 300, max: 800, largest: 800), Task: 4101304 (completed: 4101301), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://IP:20880!"

从上可以看出,到达最大线程数时,active线程数是很少的,这完全不符合预期。


五、场景模拟

由源码

 queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues))

可知:

当队列元素为0时,阻塞队列使用的是SynchronousQueue;当队列元素小于0时,使用的是无界阻塞队列LinkedBlockingQueue;当队列元素大于0时,使用的是有界的队列LinkedBlockingQueue。

核心线程数和最大线程数肯定不会有问题,所以我猜想是否队列数设置是否有问题。

为了复现,我写了个简单的代码模拟

package com.bytearch.fast.cloud;
import java.util.concurrent.*;
public class TestThreadPool {
    public final static int queueSize = 10;
    public static void main(String[] args) {
        ExecutorService executorService = getThreadPool(queueSize);
        for (int i = 0; i < 100000; i++) {
            int finalI = i;
            try {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        doSomething(finalI);
                    }
                });
            } catch (Exception e) {
                System.out.println("emsg:" + e.getMessage());
            }
            if (i % 20 == 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("all done!");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static ExecutorService getThreadPool(int queues) {
        int cores = 150;
        int threads = 800;
        int alive = 60 * 1000;
        return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)));
    }
    public static void doSomething(final int i) {
        try {
            Thread.sleep(5);
            System.out.println("thread:" + Thread.currentThread().getName() +  ", active:" + Thread.activeCount() + ", do:" + i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

模拟结果:

queueSize值 现象
0 没有出现异常
10 出现拒绝异常
100 没有出现异常

异常如下:

emsg:Task com.bytearch.fast.cloud.TestThreadPool$1@733aa9d8 rejected from java.util.concurrent.ThreadPoolExecutor@6615435c[Running, pool size = 800, active threads = 32, queued tasks = 9, completed tasks = 89755]
all done!

很显然,当并发较高时,使用LinkedBlockingQueue有界队列, 队列数设置相对较小时,线程池会出现问题。

将queues配置改为0后上线,恢复正常。

至于更深层原因,感兴趣同学可以深度分析下,也可以后台与我交流。


六、总结

这次分享了线程池 ThreadPoolExecutor 基本原理、线程池配置计算方式,和容易忽视的使用注解@Async 配置问题。

另外介绍了下我们使用线程池遇到的诡异问题,一个参数问题,可能导致不可预期的后果。

希望以上分享对你们有所帮助。

相关文章
|
2月前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
9天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
49 17
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
1月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
2月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
123 38
|
2月前
|
存储 缓存 监控
Java中的线程池深度解析####
本文深入探讨了Java并发编程中的核心组件——线程池,从其基本概念、工作原理、核心参数解析到应用场景与最佳实践,全方位剖析了线程池在提升应用性能、资源管理和任务调度方面的重要作用。通过实例演示和性能对比,揭示合理配置线程池对于构建高效Java应用的关键意义。 ####
|
2月前
|
Java
实现java执行kettle并传参数
实现java执行kettle并传参数
34 1
|
2月前
|
Java
线程池七大参数
核心线程数:线程池中的基本线程数量 最大线程数:当阻塞队列满了之后,逐一启动 最大线程的存活时间:当阻塞队列的任务执行完后,最大线长的回收时间 最大线程的存活时间单位 阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列 线程工厂:用于生产线程
|
2月前
|
Java
在Java中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
|
5月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。