一、创建线程有几种方式
在 Java 里,本质只有一条路径:创建 Thread 对象并 start()。但写法/封装常见有 4 种:
| 方式 | 说明 |
|---|---|
1. 继承 Thread |
重写 run(),再 new MyThread().start() |
2. 实现 Runnable |
new Thread(runnable).start(),推荐,解耦任务和线程 |
3. 实现 Callable + `Future |
有返回值、可抛受检异常,配合 ExecutorService |
| 4. 线程池 | Executors 工厂 或 自定义 ThreadPoolExecutor(生产环境推荐) |
补充:
Runnable:无返回值,void run()Callable<V>:有返回值,配合FutureTask或线程池的submit()
// 1. 继承 Thread(不推荐,扩展性差)
class MyThread extends Thread {
public void run() { /* ... */ }
}
// 2. Runnable(常用)
new Thread(() -> System.out.println("task")).start();
// 3. Callable
FutureTask<String> future = new FutureTask<>(() -> "result");
new Thread(future).start();
// 4. 线程池(生产推荐)
ExecutorService pool = new ThreadPoolExecutor(...);
pool.submit(() -> "result");
二、自定义线程池核心参数(ThreadPoolExecutor)
生产环境不要用 Executors.newFixedThreadPool() 等工厂方法(队列可能无界、线程数固定不合理),应显式 new ThreadPoolExecutor(...):
new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 非核心线程空闲存活时间
unit, // 时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂(命名、优先级等)
handler // 拒绝策略
);
各参数含义
| 参数 | 含义 |
|---|---|
| corePoolSize | 核心线程数。池子里常驻的线程数;即使空闲也不会被回收(除非 allowCoreThreadTimeOut=true) |
| maximumPoolSize | 最大线程数。队列满后,才会在核心线程之外再创建线程,总数不超过此值 |
| keepAliveTime | 超过 corePoolSize 的那部分线程,空闲多久后销毁 |
| workQueue | 任务等待队列。常见:ArrayBlockingQueue(有界)、LinkedBlockingQueue(可设容量)、SynchronousQueue(不存任务,直接交给线程) |
| threadFactory | 创建线程时用,便于打日志、设线程名(如 biz-pool-%d) |
| handler | 队列满且线程数已达 maximumPoolSize 时的拒绝策略 |
任务提交流程(简化)
提交任务
→ 当前线程数 < corePoolSize?→ 新建核心线程执行
→ 否则尝试入队
→ 队列满 且 线程数 < maximumPoolSize?→ 新建非核心线程执行
→ 否则走拒绝策略
四种拒绝策略
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException |
| CallerRunsPolicy | 调用者线程自己跑(起到背压作用,常用) |
| DiscardPolicy | 静默丢弃 |
| DiscardOldestPolicy | 丢弃队列里最老的任务,再提交新的 |
三、如何根据机器和业务设置参数
先分清任务类型:
| 类型 | 特点 | 线程主要在做什么 |
|---|---|---|
| CPU 密集型 | 算力、编解码、加解密、复杂计算 | 占满 CPU 时间片 |
| IO 密集型 | 查 DB、HTTP、读盘、RPC | 大量时间在等 IO,CPU 空闲 |
CPU 密集型
目标:线程数 ≈ CPU 核心数,避免过多线程导致上下文切换。
经验公式:
线程数 ≈ N_cpu + 1
或
线程数 ≈ N_cpu
N_cpu=Runtime.getRuntime().availableProcessors()或容器里的 CPU quota+1是为了某个线程偶尔阻塞(如缺页)时,其它核仍能跑满
示例(8 核):
int cpu = Runtime.getRuntime().availableProcessors(); int core = cpu; int max = cpu + 1; // 队列可以小一点,有界队列 + CallerRunsPolicy
IO 密集型
目标:线程在等 IO 时,其它线程继续用 CPU。
常见估算(假设线程约 %wait 时间在等待 IO):
线程数 ≈ N_cpu × (1 + W/C)
- W:等待时间(IO)
- C:计算时间
- 若 IO 占 90%:
N_cpu × (1 + 9) ≈ 10 × N_cpu
更实用的经验范围(无精确 profiling 时):
线程数 ≈ N_cpu × 2 ~ N_cpu × (1 + 平均阻塞时间/平均CPU时间)
例如 8 核、大量 HTTP/DB:
int cpu = Runtime.getRuntime().availableProcessors(); int core = cpu * 2; int max = cpu * 4; // 上限要设,并结合压测调
注意:不是越大越好,线程过多会增加内存、调度开销,还要受连接池、DB 最大连接数等限制。
四、实操建议(结合机器)
1. 先搞清楚“机器”是多少核
int n = Runtime.getRuntime().availableProcessors();
- 物理机:看 CPU 核心数
- Docker/K8s:看 CPU limit(
availableProcessors通常已按 limit 返回) - 混合部署:按该进程实际能用的核数算,不是整机核数
2. 分池,不要一个大池包天下
CPU 池:计算、报表、加密 → 小池,≈ N_cpu
IO 池:HTTP、DB、MQ → 大池,≈ N_cpu × 2~4(压测微调)
3. 队列要有界
new ArrayBlockingQueue<>(1000) // 或 LinkedBlockingQueue(capacity)
无界队列会导致任务堆积、OOM,且 maximumPoolSize 几乎用不上。
4. 用压测定参,而不是只套公式
| 观察指标 | 说明 |
|---|---|
| CPU 利用率 | CPU 任务池长期 100% 且队列积压 → 可能算力不够或池太小 |
| 队列长度 / 拒绝次数 | 频繁拒绝 → 加大池或优化下游 |
| 响应时间 P99 | IO 池线程够但 RT 高 → 可能是 DB/网络瓶颈,不是加线程能解决 |
| 上下文切换 | 线程过多时 vmstat 里 cs 很高 |
5. 快速对照表
| 场景 | corePoolSize | maximumPoolSize | 队列 |
|---|---|---|---|
| CPU 密集 | N_cpu | N_cpu 或 N_cpu+1 | 较小有界 |
| IO 密集 | N_cpu×2 | N_cpu×4(压测调) | 中等有界 |
| 混合型 | 拆成两个池分别配置 |
五、一句话总结
- 创建线程:继承 Thread、Runnable、Callable、线程池(生产用自定义
ThreadPoolExecutor)。 - 核心参数:
core常驻、max上限、keepAlive回收非核心线程、有界queue、handler拒绝策略。 - CPU 密集:线程数 ≈ CPU 核数;IO 密集:线程数 ≈ 核数 × (2~4 或按 W/C 估算),最终以压测和下游连接数为上限。













