线程使用的两个核心规范
首先看编程规范中, 有两个很重要的,与线程有关的需要强制执行的规范:
规范一:【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:
1)消耗内存资源:必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。
2)消耗CPU资源:需要进行系统调用,以便在OS(操作系统)中创建和注册内核线程,大量内核线程调度会导致CPU上下文过度切换。
所以,Java高并发应用频繁创建和销毁线程的操作将是非常低效的,而且是不被编程规范所允许的。
如何降低Java线程的创建成本?必须使用到线程池。使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
规范二:【强制】线程池不允许使用Executors去创建快捷线程池 ,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
-
FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
-
CachedThreadPool和ScheduledThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
通过以上规范,说明我们应用中,需要用自定义线程池。然而,由于构造一个线程池竟然有7个参数
7个重要参数中,最为重要的三个是:核心,最大线程数量, BlockingQueue。前两个参数和线程数量有关系, 后一个和内存资源消耗有关。
线程数设置太少或者阻塞队列太小, 会导致大量任务被拒绝,抛出RejectedExecutionException,触发线上的接口降级,用户体验很差。
二线程数设置太多或者阻塞队列太长,会导致资源消费高而有效负荷很小, 特别是阻塞队列设置过长,会导致频繁FullGC,甚至OOM。
如何确定系统的最佳线程数?
如何确定系统的最佳线程数,大体上分三步:
第一步,理论预估;
第二步,压测验证;
第三步,监控调整。
step1: 完成线程数的理论预估 (设计阶段)
首先,按照任务类型对线程池进行分类, 分为三类,具体如下图:
第一类:IO 密集型线程池线程数预估
线程数就是 CPU的核数的2倍。
第二类:CPU密集线程池线程数预估
CPU密集型任务并行执行的数量应当等于CPU的核心数, 线程数就是 CPU的核数
第三类:混合型线程池线程数预估
混合型线程池线程数预估, 参考下面的的公式:
最佳线程数 = ((线程等待时间 + 线程 CPU 时间) / 线程 CPU 时间 ) * CPU 核数
step2: 完成线程数的压测验证 (测试阶段)
过少的线程会造成任务拒绝,业务降级。
过多的线程会造成,额外的内存开销CPU开销,甚至会导致OOM。
所以,合理的线程池线程数,才是王道。
在设计阶段完成了step1的线程数的理论预估之后, 那么我们的理论值就出来了。
如何做验证呢?这里需要 压测。
根据公式:
服务器端最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间) * cpu数量
前面线程等待时间,线程cpu时间都是 预估的 ,都是要验证的。
首先通过用户慢慢递增来进行性能压测,观察QPS。持续大的增加用户数, 压测出最大的吞吐量。
然后再 收集 最大的吞吐量场景的 线程等待时间,线程cpu时间, 再计算出最佳线程数。
step3: 完成线程数的线上调整 (生产阶段)
压测的场景,是有限的。而线上的业务, 是复杂的,多样的。
由于系统运行过程中存在的不确定性,很难一劳永逸地规划一个合理的线程数。所以在实际线上验证的时候,需要通过实时监控预警的方式调整核心参数,这里主要可以监控的2个方面为:任务Reject次数过多、阻塞队列过载,基于这两个监控可以有依据的调整线程数(包括核心线程数、最大线程数)和阻塞队列的大小。