java线程数量如何确定
1. 概述
-
使用线程池的好处
- 降低资源消耗: 线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,通过重复利用己创建的线程可以降低线程创建和销毁造成的消耗。
- 提高响应速度: 当任务到达时,可以不需要等待线程创建就能立即执行
- 提高线程的可管理性: 线程池提供了一种限制、管理资源的策略,维护一些基本的线程统计信息,如已完成任务的数量等。通过线程池可以对线程资源进行统一的分配、监控和调优。
虽然使用线程池的好处很多,但是如果其线程数配置得不合理,不仅可能达不到预期效果,反而可能降低成用的性能
2. 线程池分类
2.1 按照任务类型对线程池进行分类
使用标准构造器ThreadPoolExecutor创建线程池时,会涉及
线程数的配置
,而线程数的配置
与异步任务类型
是分不开的。
- IO密集型任务: 此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU常处于空闲状态。Netty的IO读写操作为此类任务的典型例子。
- CPU密集型任务: 此类任务主要是执行计算任务。由于响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。
- 混合型任务: 此类任务既要执行逻辑计算,又要进行IO操作(如RPC调用、数据库访问)。相对来说,由于执行IO操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的CPU利用率也不是太高。Web服务器的HTTP请求处理操作为此类任务的典型例子。
一般情况下,针对以上不同类型的异步任务需要创建不同类型的线程池,并进行针对性的参数配置。
2.2 为 IO
密集型任务确定线程数
- 由于IO密集型任务的CPU使用率较低,导致线程空余时间很多,因此通常需要开CPU核心数两倍的线程。
- 当IO线程空闲时,可以启用其他线程继续使用CPU,以提高CPU的使用率。Netty的IO处理任务就是典型的IO密集型任务。所以,Netty的Reactor(反应器)实现类(定制版的线程池)的IO处理线程数默认正好为CPU核数的两倍
//多线程版本Reactor反应器组
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup{
//IO事件处理线程数
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1,
SystemPropertyUtil.getInt("io.netty.eventLoopThreads",
Runtime.getRuntime().availableProcessors() * 2));
}
protected MultithreadEventLoopGroup(int nThreads,
ThreadFactory threadFactory,
Object...args){
super(nThreads =0?
DEFAULT_EVENT_LOOP_THREADS : nThreads,threadFactory,args);
}
}
IO密集型demo代码
public class ThreadUtil {
/**
* CPU核数
**/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int IO_MAX = Math.max(2, CPU_COUNT * 2);
private static final int KEEP_ALIVE_SECONDS = 30;
private static final int QUEUE_SIZE = 128;
private static class IoIntenseTargetThreadPoolLazyHolder {
//线程池: 用于IO密集型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
IO_MAX,
IO_MAX,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("io"));
static {
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("IO密集型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
- IO线程池调用了allowCoreThreadTimeOut(..)方法,并且传入了参数true,keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程,当池中的线程长时间空闲时,可以自行销毁。
- 使用有界队列缓冲任务而不是无界队列,如果128太小,可以根据具体需要进行增大,但是不能使用无界队列。
- corePoolSize和maximumPoolSize不一致时,当corePoolSize满了而maximumPoolSize没满时即使可以创建线程,但是此时线程池默认不会创建线程,而是将任务加入阻塞队列,等待核心线程空闲,而如果核心线程不空闲,那么任务得不到执行。
- 如果corePoolSize和maximumPoolSize保持一致,使得在接收到新任务时,如果没有空闲工作线程,就优先创建新的线程去执行新任务,而不是将任务优先加入阻塞队列且等待现有工作线程空闲后再执行。
- 使用懒汉式单例模式创建线程池,如果代码没有用到此线程池,就不会立即创建。
- 使用JVM关闭时的钩子函数优雅地自动关闭线程池。
2.3 为 CPU 密集型任务确定线程数
- CPU密集型任务也叫
计算密集型任务
,其特点是要进行大量计算而需要消耗CPU资源,比如计算圆周率、对视频进行高清解码等。 - CPU密集型任务虽然也可以并行完成,但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用CPU,CPU密集型任务并行执行的数量应当等于CPU的核心数。比如说4个核心的CPU,通过4个线程并行执行4个CPU密集型任务,此时的效率是最高的。但是如果线程数远远超出CPU核心数量,就需要频繁地切换线程,线程上下文切换时需要消耗时间,反而会使得任务效率下降。因此,对于CPU密集型的任务来说,线程数等于CPU数就行。
public class ThreadUtil {
/**
* CPU核数
**/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int KEEP_ALIVE_SECONDS = 30;
private static final int QUEUE_SIZE = 128;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT;
private static class CpuIntenseTargetThreadPoolLazyHolder {
//线程池: 用于CPU密集型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
MAXIMUM_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("cpu"));
static {
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("CPU密集型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
}
2.4 为混合型任务确定线程数
-
混合型任务既要执行逻辑计算,又要进行大量非CPU耗时操作(如RPC调用、数据库访问、网络通信等),所以混合型任务CPU利用率不是太高,非CPU类型耗时往往是CPU耗时的数倍。比如在Web应用处理HTTP请求处理时,一次请求处理会包括DB操作、RPC操作、缓存操作等多种耗时操作。一般来说,一次Web请求的CPU计算耗时往往较少,大致在100500毫秒,而其他耗时操作会占用5001000毫秒,甚至更多的时间。
-
在为混合型任务创建线程池时,如何确定线程数呢?业界有一个比较成熟的估算公式,具体如下:
最佳线程数 = (( 线程等待时间 + 线程CPU时间 ) / 线程CPU时间 ) * CPU核数 经过简单的换算,以上公式可进一步转换为: 最佳线程数目 = ( 线程等待时间 与 线程CPU时间 之比 + 1) * CPU核数
通过公式可以看出:
- 等待时间所占比例越高,需要的线程就越多
- CPU耗时所占比例越高,需要的线程就越少。
例子:
-
比如在Web服务器处理HTTP请求时,假设平均线程CPU运行时间为100毫秒,而线程等待时间为900毫秒,如果CPU核数为8,那么根据上面这个公式,估算如下:
(900ms + 100ms) / 100ms * 8 = 10 * 8 = 80
经过计算,以上案例中需要的线程数为80。
线程数越高越好吗?使用很多线程是否就一定比单线程高效呢?答案是否定的,比如大名鼎鼎的Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/秒。
-
创建混合型线程池时,建议按照前面的最佳线程数估算公式提前预估好线程数(如80),然后设置在环境变量
mixed.thread.amount
中,测试用例如下:
public class ThreadUtil {
private static final int MIXED_CORE = 0; //混合线程池核心线程数
private static final int MIXED_MAX = 128; //最大线程数
public static final String MIXED_THREAD_AMOUNT = "mixed.thread.amount";
//懒汉式单例创建线程池:用于混合型任务
private static class MixedTargetThreadPoolLazyHolder {
//首先从环境变量 mixed.thread.amount 中获取预先配置的线程数
//如果没有对 mixed.thread.amount 做配置,则使用常量 MIXED_MAX 作为线程数
private static final int max = (null != System.getProperty(MIXED_THREAD_AMOUNT)) ?
Integer.parseInt(System.getProperty(MIXED_THREAD_AMOUNT)) : MIXED_MAX;
//线程池: 用于混合型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
max,
max,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("mixed"));
static {
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread("混合型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix