你的程序应该启动多少线程?

60 阅读19分钟

"线程数等于 CPU 核数"——这可能是程序员最耳熟能详的性能优化建议之一。

但当你真正着手设计一个系统时,你会发现事情远没有这么简单:Web 服务器动辄上千线程,游戏引擎可能只用寥寥几个,而一些高性能中间件甚至会创建 CPU 核数两倍的线程。到底谁是对的?

这篇文章试图回答一个看似简单的问题:你的程序应该启动多少线程?


一、那条广为流传的经验法则

几乎每本并发编程的书都会告诉你:

对于 CPU 密集型任务,线程数应等于 CPU 核数(或核数 + 1)。

这条规则背后的逻辑很直观:每个 CPU 核心同一时刻只能执行一个线程。如果线程数超过核心数,多余的线程只能等待,还会带来额外的上下文切换开销。如果线程数少于核心数,又会让部分核心空转。

这条规则没有错,但它只回答了一个非常狭窄的问题:当你的唯一目标是最大化 CPU 利用率时,应该用多少线程?

现实中的软件系统要复杂得多。


二、线程的真实作用:不只是并行

当我们谈论"为什么需要线程"时,教科书往往只强调一点:并行计算。但在实际工程中,线程至少承担着三种截然不同的职责:

1. 通过异步避免阻塞

想象一个 GUI 程序:用户点击按钮后,程序需要从网络加载数据。如果在主线程中同步等待网络响应,整个界面就会冻结。

// 糟糕的做法:阻塞主线程
void onClick() {
    Data data = network.fetchSync();  // 界面卡住 3 秒
    updateUI(data);
}
​
// 更好的做法:用单独线程处理阻塞操作
void onClick() {
    new Thread(() -> {
        Data data = network.fetchSync();
        runOnUIThread(() -> updateUI(data));
    }).start();
}

这里的线程不是为了并行计算,而是为了不阻塞主线程。即使在单核 CPU 上,这种设计也是有意义的。

2. 故障隔离:舱壁模式

在微服务架构中,一个服务可能依赖多个下游系统。如果所有请求共享同一个线程池,当某个下游系统变慢时,线程会被逐渐耗尽,最终导致整个服务不可用——这就是级联故障

┌─────────────────────────────────────────────────┐
│                   共享线程池                      │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐       │
│  │ T1  │ │ T2  │ │ T3  │ │ T4  │ │ T5  │       │
│  │阻塞 │ │阻塞 │ │阻塞 │ │阻塞 │ │阻塞 │       │
│  └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘       │
│     │       │       │       │       │          │
│     └───────┴───────┼───────┴───────┘          │
│                     ▼                           │
│         下游服务 A(响应变慢)                    │
│                                                 │
│  结果:所有线程被 A 占满,B 和 C 的请求无法处理    │
└─────────────────────────────────────────────────┘

舱壁模式(Bulkhead Pattern)借鉴了船舶设计的思想:将船体分隔成多个水密舱,一个舱室进水不会导致整艘船沉没。

┌──────────────────────────────────────────────────┐
│                                                  │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│   │ 线程池 A  │  │ 线程池 B  │  │ 线程池 C  │      │
│   │ (3线程)  │  │ (3线程)  │  │ (3线程)  │      │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘      │
│        │             │             │            │
│        ▼             ▼             ▼            │
│    下游服务A      下游服务B      下游服务C        │
│    (变慢)        (正常)        (正常)           │
│                                                  │
│   结果:A 的线程池耗尽,但 B 和 C 不受影响        │
└──────────────────────────────────────────────────┘

这种设计会让总线程数远超 CPU 核数,但换来的是系统的韧性

3. 简化抽象:让代码更易理解

有时候,多线程的目的纯粹是为了代码组织

考虑一个游戏服务器需要同时处理:

  • 网络消息收发
  • 游戏逻辑 tick
  • 定时任务调度
  • 日志异步写入
  • 监控指标上报

你当然可以用一个复杂的事件循环把它们全部塞进单线程:

while True:
    if has_network_event():
        handle_network()
    if time_for_game_tick():
        game_tick()
    if has_scheduled_task():
        run_task()
    if log_buffer_not_empty():
        flush_logs()
    # ... 代码很快变成一团乱麻

但更清晰的做法是为每个职责分配独立的线程:

Thread("network",   network_loop)
Thread("game-tick", game_loop)
Thread("scheduler", scheduler_loop)
Thread("logger",    log_writer_loop)

这些线程大部分时间可能都在 sleep 或等待 I/O,根本不争抢 CPU。但它们让代码结构变得清晰:每个线程有明确的职责和生命周期


三、线程的真实开销

既然线程有这么多用途,是不是可以随意创建?在回答这个问题之前,我们需要理解线程在现代操作系统中的真实开销。

1. 创建开销

创建一个线程需要:

  • 分配内核数据结构(Linux 上是 task_struct,约 2-3 KB)
  • 分配用户态栈空间
  • 在调度器中注册
  • 各种安全检查和初始化

在 Linux 上,创建一个线程大约需要 10-30 微秒。这个开销对于长生命周期的线程可以忽略,但如果频繁创建销毁(如"每个请求一个线程"的模型),累积起来就相当可观。

// 简单测试:创建 10000 个线程
for (int i = 0; i < 10000; i++) {
    pthread_create(&threads[i], NULL, empty_func, NULL);
}
// 在普通机器上可能需要 100-300ms

2. 内存占用

每个线程需要独立的栈空间。默认配置下:

  • Linux:8 MB(虚拟内存),实际物理内存按需分配
  • Windows:1 MB(commit)
  • macOS:512 KB(主线程 8 MB)

1000 个线程,按 Linux 默认配置,光栈空间就需要 8 GB 的虚拟地址空间。虽然物理内存是惰性分配的,但这个数字仍然令人警醒。

你可以通过调整栈大小来优化:

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 256 * 1024);  // 256 KB
pthread_create(&thread, &attr, func, NULL);

或者在 Java 中:

Thread thread = new Thread(null, runnable, "name", 256 * 1024);
// 或者 JVM 参数 -Xss256k

3. 上下文切换

这是最常被提及的开销。当 CPU 从执行线程 A 切换到线程 B 时,需要:

  1. 保存现场:寄存器状态、程序计数器、栈指针等
  2. 切换页表(如果是不同进程的线程)
  3. 恢复现场:加载线程 B 的状态
  4. 缓存失效:这往往是最大的隐藏开销

纯粹的上下文切换本身只需要 1-5 微秒。但切换后,新线程访问的数据很可能不在 CPU 缓存中,需要从内存重新加载。这种缓存污染导致的性能损失可能比切换本身高出一个数量级。

┌─────────────────────────────────────────────────────┐
│                  上下文切换的真实开销                 │
├─────────────────────────────────────────────────────┤
│  直接开销(保存/恢复状态)         ~1-5 μs          │
│  间接开销(缓存失效)              ~10-100 μs       │
│  总体影响                          视工作负载而定   │
└─────────────────────────────────────────────────────┘

4. 调度开销

操作系统需要维护运行队列、就绪队列,执行调度算法来决定下一个运行的线程。线程数越多,调度器的负担越重。

在极端情况下(数万线程),光是遍历调度队列都会成为性能瓶颈。

量化视角

让我们把这些数字放在一起:

开销类型量级影响
创建线程10-30 μs频繁创建时累积
栈内存256KB-8MB/线程限制最大线程数
上下文切换1-5 μs高频切换时累积
缓存失效10-100 μs最大的隐藏开销

对于大多数应用来说,几十到几百个线程是完全可以接受的。现代 Linux 内核可以轻松处理上万个线程,只要它们不都在同时争抢 CPU。


四、决策框架:按用途确定线程数

理解了线程的多重作用和真实开销后,我们可以建立一个决策框架:

1. CPU 密集型计算:等于核数

如果线程的主要工作是计算(数学运算、数据处理、加密解密等),坚守经典法则:

int threadCount = Runtime.getRuntime().availableProcessors();
// 或者 核数 + 1,留一个处理偶发的 I/O

这里的逻辑很简单:更多的线程只会增加切换开销,不会提升吞吐量。

实际案例

  • 图像处理库
  • 科学计算框架
  • 视频编解码器

2. I/O 密集型:优先考虑非阻塞

如果线程大部分时间在等待 I/O(网络、磁盘、数据库),你有两个选择:

选项 A:非阻塞 I/O + 事件循环(推荐)

# Python asyncio
async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        return await asyncio.gather(*tasks)
        
# 用少量线程处理大量并发连接

Node.js、Nginx、Redis 都采用这种模型,用极少的线程处理海量并发。

选项 B:线程池 + 阻塞 I/O

如果你必须使用阻塞 I/O(比如 JDBC 不支持异步),可以用经典公式:

线程数 = CPU 核数 × (1 + I/O 等待时间 / CPU 计算时间)

如果平均每个请求花 100ms 等待 I/O、1ms 做计算,在 8 核机器上:

线程数 = 8 × (1 + 100/1) = 808

这只是理论上限。实际中还要考虑:

  • 下游系统能否承受这么多并发
  • 内存是否足够
  • 连接池大小限制

3. 故障隔离:按风险域划分

为每个可能独立失败的依赖分配独立的线程池:

// Hystrix/Resilience4j 风格
ThreadPoolBulkhead paymentPool = ThreadPoolBulkhead.of("payment", 
    ThreadPoolBulkheadConfig.custom()
        .maxThreadPoolSize(10)
        .coreThreadPoolSize(5)
        .queueCapacity(20)
        .build());
​
ThreadPoolBulkhead inventoryPool = ThreadPoolBulkhead.of("inventory", 
    ThreadPoolBulkheadConfig.custom()
        .maxThreadPoolSize(8)
        .coreThreadPoolSize(4)
        .queueCapacity(15)
        .build());
​
ThreadPoolBulkhead shippingPool = ThreadPoolBulkhead.of("shipping", 
    ThreadPoolBulkheadConfig.custom()
        .maxThreadPoolSize(6)
        .coreThreadPoolSize(3)
        .queueCapacity(10)
        .build());

每个池的大小取决于:

  • 下游服务的正常响应时间:响应越慢,需要越多线程来维持吞吐量
  • 可接受的最大并发数:下游服务能承受多少并发请求
  • 降级策略:线程池满时是快速失败、排队等待,还是返回降级结果

计算舱壁大小的方法

假设某个下游服务:

  • 正常响应时间:50ms
  • 期望吞吐量:每秒 100 个请求
  • 可接受的排队延迟:100ms
最小线程数 = 吞吐量 × 响应时间
          = 100 × 0.05
          = 5 个线程
​
队列容量 = 吞吐量 × 可接受排队延迟
        = 100 × 0.1
        = 10 个请求

但还要考虑异常情况。如果下游服务变慢(响应时间从 50ms 变成 500ms):

此时需要的线程数 = 100 × 0.5 = 50 个线程

这就是舱壁要保护的场景。我们不应该给它 50 个线程,而是:

ThreadPoolBulkhead.of("payment",
    ThreadPoolBulkheadConfig.custom()
        .maxThreadPoolSize(10)      // 硬上限:最多 10 个线程
        .coreThreadPoolSize(5)      // 正常情况够用
        .queueCapacity(20)          // 允许短暂排队
        .build());
​
// 当下游变慢时:
// - 10 个线程被占满
// - 20 个请求在队列等待
// - 第 31 个请求立即被拒绝(快速失败)
// - 系统其他部分不受影响

舱壁 vs 断路器

舱壁模式常与断路器(Circuit Breaker)配合使用:

┌─────────────────────────────────────────────────────────┐
│                        请求流程                          │
│                                                         │
│   请求 ──→ 断路器 ──→ 舱壁(线程池) ──→ 下游服务        │
│              │              │                           │
│              │              │                           │
│         检查是否熔断    检查是否有空闲线程                │
│              │              │                           │
│              ▼              ▼                           │
│         熔断则快速失败  无空闲则拒绝或排队                │
│                                                         │
└─────────────────────────────────────────────────────────┘
// Resilience4j 组合使用示例
CircuitBreaker circuitBreaker = CircuitBreaker.of("payment", 
    CircuitBreakerConfig.custom()
        .failureRateThreshold(50)           // 失败率超过 50% 则熔断
        .waitDurationInOpenState(Duration.ofSeconds(30))
        .build());
​
ThreadPoolBulkhead bulkhead = ThreadPoolBulkhead.of("payment", ...);
​
Supplier<Response> decoratedSupplier = Decorators
    .ofSupplier(() -> paymentService.call())
    .withCircuitBreaker(circuitBreaker)     // 先检查断路器
    .withThreadPoolBulkhead(bulkhead)       // 再进入线程池
    .withFallback(ex -> fallbackResponse()) // 降级响应
    .decorate();

舱壁的代价

舱壁模式会显著增加系统的总线程数:

传统模式:
  1 个共享线程池 × 50 线程 = 50 线程
​
舱壁模式:
  支付服务池    10 线程
  库存服务池     8 线程
  物流服务池     6 线程
  用户服务池     8 线程
  通知服务池     4 线程
  ─────────────────────
  总计          36 线程(但每个池都有冗余)
  
  实际配置时考虑峰值:
  每个池 ×1.5 = 54 线程

这看起来线程更多了,但关键区别在于:

对比项共享线程池舱壁模式
总线程数较少较多
单点故障影响整个系统仅一个服务
资源利用率更高有冗余浪费
容量规划简单需要分别规划
故障恢复慢(需等待所有线程释放)快(其他池不受影响)

在微服务架构中,隔离性通常比资源利用率更重要。舱壁带来的额外线程开销,换来的是系统在部分故障时仍能提供服务的能力。

4. 简化抽象:按职责最小化

当线程用于代码组织时,遵循够用就好原则:

// 典型的服务端应用线程分配
Thread acceptor    = new Thread(this::acceptLoop);     // 1个:接受连接
Thread[] workers   = new Thread[cpuCores];             // N个:处理业务
Thread timer       = new Thread(this::timerLoop);      // 1个:定时任务
Thread logger      = new Thread(this::logWriter);      // 1个:异步日志
Thread monitor     = new Thread(this::metricsReport);  // 1个:监控上报// 总计:cpuCores + 4 个线程

这些辅助线程大部分时间在休眠,不会与 worker 线程竞争 CPU。关键是确保它们:

  • 不会执行耗时的计算
  • 不会频繁唤醒
  • 有明确的单一职责

五、警惕"线程风暴"

即使每个决策单独看都合理,累积起来也可能造成问题。

叠加效应

假设你的 Java 服务:

  • Tomcat 线程池:200 个
  • 数据库连接池:每个连接有后台线程,50 个
  • Redis 客户端池:20 个
  • Kafka 消费者:10 个分区 × 3 个消费者组 = 30 个
  • 定时任务调度器:核心线程 10 个
  • JVM GC 线程:8 个
  • 其他框架的后台线程:若干

加起来可能有 300-500 个线程,而你的机器可能只有 8 个 CPU 核心。

抖动风险

在某些时刻,大量线程可能同时被唤醒:

┌─────────────────────────────────────────────────────┐
              t0: 某个事件触发                        
                                                    
     ┌────────────────┼───────────────┐              
                                                  
  ┌─────┐         ┌─────┐         ┌─────┐           
  │100个│         │50个          │30个            
  │HTTP          │定时          │Kafka│           
  │请求          │任务          │消息            
  └──┬──┘         └──┬──┘         └──┬──┘           
                                                  
     └───────────────┼───────────────┘               
                                                    
           8  CPU 核心开始疯狂切换                  
                                                    
                                                    
        延迟飙升,GC 停顿,服务抖动                   
└─────────────────────────────────────────────────────┘

这种"线程风暴"会导致:

  • 所有请求的延迟同时上升
  • P99 延迟剧烈波动
  • 可能触发超时和级联故障

缓解策略

策略一:错峰调度

让定时任务随机分散,而不是整点触发:

// 不好:所有实例同时执行
@Scheduled(cron = "0 0 * * * *")  // 每小时整点
​
// 更好:启动时计算随机偏移
int jitter = random.nextInt(60);
@Scheduled(cron = "0 " + jitter + " * * * *")

策略二:为关键线程提升优先级

确保 CPU 密集型的核心工作线程能优先获得调度:

// 关键业务线程
thread.setPriority(Thread.MAX_PRIORITY);  // Java: 1-10,默认 5// 或者在 Linux 上使用 nice 值
// nice -n -5 java -jar app.jar
// C/C++:使用实时调度策略
struct sched_param param;
param.sched_priority = 50;  // 实时优先级
pthread_setschedparam(thread, SCHED_FIFO, &param);

策略三:CPU 绑定(CPU Affinity)

将关键线程绑定到特定 CPU,避免缓存失效:

// 使用 JNA 或 JNI 调用系统 API
// Linux: sched_setaffinity()
// 或使用 Disruptor 等框架内置的支持
// C: 将线程绑定到 CPU 0 和 1
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);
pthread_setaffinity_np(thread, sizeof(cpuset), &cpuset);

策略四:限制并发

使用信号量或令牌桶限制同时运行的线程数:

Semaphore semaphore = new Semaphore(cpuCores * 2);
​
void process(Request request) {
    semaphore.acquire();
    try {
        doWork(request);
    } finally {
        semaphore.release();
    }
}

六、协程时代:换汤不换药?

Go 语言的 goroutine、Kotlin 的协程、Java 的虚拟线程(Project Loom)……协程似乎成了并发的银弹。

"创建一百万个协程"的 demo 随处可见,这是否意味着我们不用再关心"数量"问题了?

协程的本质

协程(或用户态线程、绿色线程)本质是把调度权从操作系统收回到用户态

┌───────────────────────────────────────────────────┐
│                    传统线程                        │
│                                                   │
│   线程1  线程2  线程3  线程4  ... 线程1000        │
│     │      │      │      │           │           │
│     └──────┴──────┴──────┴───────────┘           │
│                    │                              │
│            操作系统调度器                          │
│                    │                              │
│     ┌──────┬──────┬──────┬──────────┐            │
│     ▼      ▼      ▼      ▼          ▼            │
│   CPU0   CPU1   CPU2   CPU3  ...  CPU7           │
└───────────────────────────────────────────────────┘
​
┌───────────────────────────────────────────────────┐
│                    协程模型                        │
│                                                   │
│   协程1  协程2  协程3  ... 协程1000000            │
│     │      │      │            │                 │
│     └──────┴──────┴────────────┘                 │
│                    │                              │
│           语言运行时调度器                         │
│                    │                              │
│     ┌──────────────┼──────────────┐              │
│     ▼              ▼              ▼              │
│   线程1          线程2    ...   线程N            │
│     │              │              │              │
│     └──────────────┼──────────────┘              │
│                    │                              │
│            操作系统调度器                          │
│                    │                              │
│            ┌───────┴───────┐                     │
│            ▼               ▼                     │
│          CPU0    ...    CPU7                     │
└───────────────────────────────────────────────────┘

协程的优势在于:

  • 创建开销极小:Go 的 goroutine 初始栈只有 2KB
  • 切换开销极小:用户态切换,不需要进入内核
  • 调度更智能:运行时了解协程在做什么(如等待 channel)

但物理规则依然适用

协程改变的是切换效率,不是 CPU 核心数

// 100 万个协程同时做 CPU 密集计算?
for i := 0; i < 1000000; i++ {
    go func() {
        for {
            // 纯计算,没有 I/O
            heavyComputation()
        }
    }()
}
// 结果:并不会比 GOMAXPROCS 个协程更快

核心洞察:如果协程在执行时不主动让出(通过 I/O、channel、sleep 等),它就和操作系统线程没有本质区别

协程的正确心智模型

操作类型协程行为考量
I/O 等待挂起,让出执行权可以有百万并发
Channel 等待挂起,让出执行权可以有百万并发
CPU 计算持续占用线程同时计算数 ≈ GOMAXPROCS
调用 C 代码可能阻塞线程可能需要更多线程

Go 运行时会自动调整实际的操作系统线程数,但 GOMAXPROCS(默认等于 CPU 核数)限制了同时执行的线程数。

// 正确用法:百万协程处理 I/O
for i := 0; i < 1000000; i++ {
    go handleConnection(conn[i])  // 每个协程大部分时间在等待网络
}
​
// 需要注意:CPU 密集型任务
pool := make(chan struct{}, runtime.NumCPU())  // 信号量
for task := range tasks {
    pool <- struct{}{}  // 获取令牌
    go func(t Task) {
        defer func() { <-pool }()  // 释放令牌
        cpuIntensiveWork(t)        // 同时只有 NumCPU 个在计算
    }(task)
}

Java 虚拟线程的启示

Java 21 引入的虚拟线程(Virtual Threads)同样遵循这个逻辑:

// 可以创建大量虚拟线程处理阻塞 I/O
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100000; i++) {
        executor.submit(() -> {
            // 阻塞 I/O 会自动让出载体线程
            String result = httpClient.send(request);
            process(result);
        });
    }
}
​
// 但如果是 CPU 密集型...
executor.submit(() -> {
    // 这个虚拟线程会持续占用载体线程
    while (true) {
        computePi();  // 其他虚拟线程饿死
    }
});

七、实践清单

让我们把讨论转化为可操作的检查清单:

启动前问自己

□ 这个线程/协程的主要工作是什么?
  - [ ] CPU 计算
  - [ ] I/O 等待
  - [ ] 故障隔离
  - [ ] 代码组织
​
□ 它会阻塞吗?阻塞多久?
​
□ 它需要和其他线程竞争资源吗?
​
□ 它的生命周期是什么?
  - [ ] 与应用相同(后台线程)
  - [ ] 与请求相同(per-request)
  - [ ] 执行完任务就结束(fire-and-forget)

配置建议速查

场景线程数建议关键考量
纯 CPU 计算= 核数更多无益
CPU 计算 + 偶发 I/O= 核数 + 1~2应对偶发阻塞
I/O 密集(非阻塞)核数或更少事件循环处理并发
I/O 密集(阻塞)取决于 I/O 时间比例用公式估算,压测验证
舱壁隔离每依赖一个独立池池大小取决于下游容量
辅助功能每职责 1 个确保不争抢 CPU

监控指标

上线后,持续关注:

线程状态分布
├── RUNNABLE(运行中)   → 应该 ≈ CPU 核数
├── BLOCKED(锁等待)    → 过高说明有锁竞争
├── WAITING(条件等待)  → I/O 线程正常状态
└── TIMED_WAITING(超时等待)→ sleep 或 poll
​
上下文切换率
└── vmstat, pidstat -w  → 每秒切换数
​
线程创建率
└── 高频创建说明需要用池
​
CPU 使用率
└── 高于预期 → 检查是否在自旋
└── 低于预期 → 检查是否在等锁

八、总结

回到最初的问题:你的程序应该启动多少线程?

答案是:取决于这些线程要做什么

  • 如果是为了并行计算,线程数应该接近 CPU 核心数
  • 如果是为了处理阻塞 I/O,优先考虑非阻塞方案;如果必须阻塞,根据 I/O 时间比例估算
  • 如果是为了故障隔离,为每个风险域分配独立的资源边界
  • 如果是为了简化代码,确保这些线程不会争抢关键资源

"线程数 = CPU 核心数"是一个好的起点,但不是终点。理解你的工作负载,理解线程的真实开销,理解你想通过多线程解决的问题——这比任何公式都重要。

最后,无论你做出什么选择,记得压测验证。真实世界的表现总是比理论分析更复杂,而性能问题往往藏在那些"理论上应该没问题"的地方。


附录:常见框架的默认配置参考

了解你正在使用的框架的默认行为,有助于做出更好的决策。

Web 服务器

框架/服务器默认线程配置说明
Tomcat最大 200,最小 10每个请求一个线程
Jetty最大 200QueuedThreadPool
Undertow核数 × 8(I/O)+ 核数(Worker)非阻塞架构
Netty核数 × 2(EventLoop)事件驱动,少量线程
Go net/http无限制(goroutine)每连接一个 goroutine
Node.js1(主线程)+ 4(libuv 线程池)单线程事件循环
Nginxworker 数通常 = 核数每个 worker 单线程事件循环

数据库连接池

连接池默认配置推荐起点
HikariCP最大 10核数 × 2 + 磁盘数
Druid最大 8根据并发量调整
c3p0最大 15通常偏保守
pgBouncer取决于模式transaction 模式更高效

HikariCP 作者给出的经验公式:

连接数 = ((核心数 × 2) + 有效磁盘数)

对于大多数场景,10-20 个连接足以支撑相当高的吞吐量。更多的连接往往意味着更多的锁竞争和上下文切换,反而降低性能。

消息队列客户端

客户端默认配置注意事项
Kafka Consumer每分区一个线程分区数决定并行度上限
RabbitMQ Consumer可配置 prefetch控制未确认消息数
Redis (Lettuce)共享连接 + 核数个事件线程非阻塞,高效
Redis (Jedis)连接池,每操作占用一个阻塞模型

线程池最佳实践

// 推荐:根据任务类型创建不同的线程池
// 而不是所有任务共享一个// CPU 密集型任务
ExecutorService cpuPool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors(),
    new ThreadFactoryBuilder().setNameFormat("cpu-worker-%d").build()
);
​
// I/O 密集型任务
ExecutorService ioPool = new ThreadPoolExecutor(
    corePoolSize,     // 核心线程数
    maxPoolSize,      // 最大线程数
    60, TimeUnit.SECONDS,  // 空闲线程存活时间
    new LinkedBlockingQueue<>(queueCapacity),  // 有界队列!
    new ThreadFactoryBuilder().setNameFormat("io-worker-%d").build(),
    new CallerRunsPolicy()  // 拒绝策略:让调用者自己执行
);
​
// 定时任务
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(
    2,  // 通常不需要很多
    new ThreadFactoryBuilder().setNameFormat("scheduler-%d").build()
);

关键提醒:永远使用有界队列和合理的拒绝策略。无界队列在高负载下会导致内存溢出。