这是「Java 从基础到实战」系列的第 7 篇,进入阶段二「进阶硬核」。本文从创建线程的两种方式讲起,重点拆解 ThreadPoolExecutor 的 7 个构造参数——每个参数都用代码实验演示效果,而不是背概念。所有代码在 JDK 17 下可直接运行。
前言
面试必问「线程池 7 个参数」,但很多人背得出说不清。本文用一个可以调参观察的实验类,让你亲眼看到核心线程、队列、最大线程、拒绝策略是按什么顺序生效的。
环境准备
| 软件 |
版本 |
| JDK |
17(Eclipse Temurin) |
| IntelliJ IDEA |
Community 社区版 |
在 java-basics 项目中新建包 thread。
步骤 1:创建线程的两种基本方式
新建 CreateThreadDemo.java:
package thread;
public class CreateThreadDemo { public static void main(String[] args) { Thread t1 = new MyThread(); t1.start();
Thread t2 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 在执行任务"); }, "worker-2"); t2.start();
System.out.println(Thread.currentThread().getName() + " 是主线程"); }
static class MyThread extends Thread { @Override public void run() { System.out.println(getName() + " 在执行任务"); } } }
|
预期输出(顺序可能不同——这正是并发的特点):
main 是主线程 Thread-0 在执行任务 worker-2 在执行任务
|
⚠️ 调用 run() 只是普通方法调用,仍在当前线程执行;start() 才会启动新线程。这是新手第一坑。
步骤 2:为什么需要线程池
每次 new Thread 的问题:创建/销毁线程开销大(约 1ms 级 + 默认 1MB 栈内存)、数量不受控(来 1 万个请求就建 1 万个线程,直接把机器拖垮)。
线程池的思路:预先养一批线程反复使用,任务多了排队,队也排满了再按策略处理。
步骤 3:ThreadPoolExecutor 的 7 个参数
public ThreadPoolExecutor( int corePoolSize, // 1. 核心线程数:常驻员工 int maximumPoolSize, // 2. 最大线程数:忙时可临时扩招的上限 long keepAliveTime, // 3. 临时工空闲多久被辞退 TimeUnit unit, // 4. 上面时间的单位 BlockingQueue<Runnable> workQueue, // 5. 任务队列:排队区 ThreadFactory threadFactory, // 6. 线程工厂:给线程起名 RejectedExecutionHandler handler // 7. 拒绝策略:队满且线程到顶时怎么办 )
|
任务提交的真实顺序(重点,面试常被追问):
来任务 → 核心线程未满?建核心线程执行 → 核心满了?进队列排队 → 队列也满了?建临时线程(直到 maximumPoolSize) → 线程也到顶了?执行拒绝策略
|
注意:是先排队,再扩容,不是先扩容再排队。
步骤 4:用实验验证参数行为
新建 PoolExperiment.java,参数刻意调小方便观察:
package thread;
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger;
public class PoolExperiment { public static void main(String[] args) { AtomicInteger threadNo = new AtomicInteger(1); ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 4, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), r -> new Thread(r, "worker-" + threadNo.getAndIncrement()), new ThreadPoolExecutor.AbortPolicy() );
for (int i = 1; i <= 7; i++) { final int taskId = i; try { pool.execute(() -> { System.out.printf("任务%d 由 %s 执行%n", taskId, Thread.currentThread().getName()); sleep(3000); }); System.out.printf("任务%d 提交成功,当前线程数=%d,队列长度=%d%n", taskId, pool.getPoolSize(), pool.getQueue().size()); } catch (RejectedExecutionException e) { System.out.printf("任务%d 被拒绝!%n", taskId); } } pool.shutdown(); }
static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
|
预期输出(关键部分):
任务1 提交成功,当前线程数=1,队列长度=0 ← 建核心线程 1 任务2 提交成功,当前线程数=2,队列长度=0 ← 建核心线程 2 任务3 提交成功,当前线程数=2,队列长度=1 ← 核心满,进队列 任务4 提交成功,当前线程数=2,队列长度=2 ← 继续排队,队列满 任务5 提交成功,当前线程数=3,队列长度=2 ← 队列满,扩临时线程 3 任务6 提交成功,当前线程数=4,队列长度=2 ← 扩临时线程 4,到达最大 任务7 被拒绝! ← 全满,触发 AbortPolicy
|
一次实验把「先排队再扩容、最后拒绝」的顺序看得清清楚楚。
四种内置拒绝策略速记:
| 策略 |
行为 |
适用 |
AbortPolicy(默认) |
抛异常 |
关键业务,快速失败 |
CallerRunsPolicy |
提交者自己执行,天然限流 |
不允许丢任务 |
DiscardPolicy |
静默丢弃 |
可丢弃的任务(如日志) |
DiscardOldestPolicy |
丢队头最老的任务 |
新数据比旧数据重要 |
步骤 5:获取任务结果 —— submit 与 Future
execute 没有返回值,需要结果时用 submit + Callable:
package thread;
import java.util.concurrent.*;
public class FutureDemo { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(() -> { Thread.sleep(1000); return 1 + 1; });
System.out.println("主线程先干别的……"); System.out.println("计算结果: " + future.get(2, TimeUnit.SECONDS)); pool.shutdown(); } }
|
💡 future.get() 会阻塞等待。更优雅的异步编排(多任务并行、结果聚合)留给第 9 篇的 CompletableFuture。
常见坑
坑 1:用 Executors 快捷工厂创建线程池
错误示范 ❌:
ExecutorService pool = Executors.newFixedThreadPool(10); ExecutorService pool2 = Executors.newCachedThreadPool();
|
原因:newFixedThreadPool 的队列是无界 LinkedBlockingQueue,任务堆积直接 OOM;newCachedThreadPool 最大线程数是 Integer.MAX_VALUE,线程爆炸。《阿里巴巴 Java 开发手册》强制禁止。
正确写法:手动 new ThreadPoolExecutor,显式指定有界队列和拒绝策略 ✅(见步骤 4)。
坑 2:线程池不 shutdown,程序退不出去
现象:main 方法跑完了,进程还挂着。原因:池内是非守护线程,JVM 等它们结束。
正确写法:不再提交任务时调用 pool.shutdown()(温和,等已提交任务跑完);需要立刻停用 shutdownNow() ✅。
坑 3:任务里的异常被静默吞掉
错误示范 ❌:
pool.submit(() -> { throw new RuntimeException("出错了"); });
|
正确写法:submit 的任务必须 future.get() 检查;或改用 execute(异常会打到控制台);或任务内部自行 try-catch 记日志 ✅。
小结
- ✅ 创建线程用 Runnable + Lambda,
start() 而非 run()
- ✅ 7 参数核心顺序:核心线程 → 队列 → 临时线程 → 拒绝策略
- ✅ 禁用 Executors 快捷工厂,手动创建并指定有界队列
- ✅
submit + Future 拿结果,记得处理任务内异常
下一篇聚焦线程安全:《Java 并发编程实战:synchronized 与 ReentrantLock 对比》,手写卖票 demo 复现超卖问题再逐步修复,敬请期待。
上一篇:《Java IO 与 NIO 入门:文件读写的 5 种方式及性能对比》
本系列完整目录见博客「技术博文」分类。
💬 评论