直接new Thread()不适合高并发场景,因频繁创建销毁线程导致栈内存分配、GC压力、调度竞争及OS线程耗尽;Executor框架通过复用线程、统一管理生命周期、解耦任务提交与执行来优化。
频繁创建和销毁线程会带来显著的系统开销:JVM 需要分配栈内存、触发 GC、参与线程调度竞争。在 QPS 较高的服务中,new Thread(runnable).start() 很快会耗尽 OS 线程资源(Linux 默认每进程约 1024 个线程),抛出 java.lang.OutOfMemoryError: unable to create new native thread。
Executor 框架通过复用线程、统一管理生命周期、解耦任务提交与执行,把“怎么跑”交给线程池,“跑什么”由业务代码决定。
Executors 工具类创建的线程池有陷阱:比如 Executors.newFixedThreadPool(n) 底层用的是无界 LinkedBlockingQueue,任务堆积会导致 OOMThreadPoolExecutor,明确控制核心线程数、队列容量、拒绝策略核心不是背参数名,而是理解它们如何协同影响吞吐与稳定性。以 Web 请求处理为例:
corePoolSize:常驻线程数,建议设为 CPU 核心数 × (1 + 平均等待时间 / 平均执行时间),IO 密集型可适当放大maximumPoolSize:最大线程数,避免设得过大(如 1000+),否则上下文切换开销反超收益;一般不超过 corePoolSize × 2
workQueue:优先选有界队列,如 ArrayBlockingQueue(100);避免 SynchronousQueue 在突发流量下直接触发拒绝策略handler:别用默认的 AbortPolicy(直接抛 RejectedExecutionException);更稳妥的是 CallerRunsPolicy(让调用线程自己执行任务,自然降速)或自定义记录+告警ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize
8,
// maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new ArrayBlockingQueue(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);
execute(Runnable) 是 Executor 接口方法,只负责提交,不关心结果;submit() 是 ExecutorService 扩展方法,支持获取执行结果和异常。
submit(Runnable) 时,返回的 Future 调用 get() 只会返回 null,但能捕获运行时异常(比如空指针)submit(Callable) 才真正发挥异步计算价值:Future f = executor.submit(() -> "done"); String result = f.get();
Future.get() 是阻塞的;若需非阻塞,可用 isDone() 轮询,或结合 CompletableFuture 做链式回调get() 或未处理 ExecutionException,会导致异常“静默丢失”两者都不保证立即终止所有线程,只是改变线程池状态并尝试中断正在运行的任务。
shutdown():设置 RUNNING → SHUTDOWN,不再接受新任务,但会等已提交任务(包括队列中等待的)全部执行完shutdownNow():设置 RUNNING → STOP,尝试中断所有正在执行的线程,并清空队列,返回未执行的任务列表Thread.interrupted() 或捕获 InterruptedException,否则 shutdownNow() 对该线程无效while (true) { doWork(); } —— 它永远不响应中断;应改为 while (!Thread.currentThread().isInterrupted()) { doWork(); }
实际关闭流程建议加超时等待:
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
线程池的边界往往藏在 shutdown 逻辑里——没等完就退出、没处理中断、没清理资源,都会让应用在重启或降级时留下残留线程。