跳到内容

元数据卡

  • 前置知识:并发同步(ch14-concurrency-sync)、线程创建(ch14-concurrency-intro)
  • 预计时间:20 分钟
  • 完成标志:能用 ExecutorService 管理并发任务,理解线程池核心参数,知道如何优雅关闭

铁匠铺生意好了。每来一个客人,你就招一个学徒。来 100 个客人你就招 100 个学徒——但工坊只有 4 个工位。更糟的是,招一个学徒要准备工具、交待规矩——开销比打铁本身还大。

每次都 new Thread() 来跑任务,就有这个问题:线程创建和销毁有成本,大量短线程甚至拖垮系统。

线程池:提前招好工人

方案很简单:提前招好 4 个工人,让他们站在工位前等着。来了任务就干,干完了等下一个。

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            pool.submit(() -> {
                System.out.println("任务 " + taskId
                    + " 由 " + Thread.currentThread().getName() + " 执行");
                try { Thread.sleep(500); } catch (InterruptedException e) { }
            });
        }

        pool.shutdown();
        System.out.println("主线程结束——池子里的任务还在跑");
    }
}

语言: Java 5+ 运行: 编译运行 预期输出:

任务 1 由 pool-1-thread-1 执行
任务 2 由 pool-1-thread-2 执行
...
主线程结束——池子里的任务还在跑
任务 5 由 pool-1-thread-1 执行  ← 线程复用!
...

Executors 工厂方法

Executors 提供了几种预设线程池:

工厂方法线程数队列什么时候用
newFixedThreadPool(n)固定 n 个无界队列任务量可控
newCachedThreadPool()动态增减不排队大量短任务
newSingleThreadExecutor()1 个无界队列顺序执行
newScheduledThreadPool(n)固定 n 个定时/周期任务

Callable 和 Future:任务有返回值

有时你需要任务返回结果:

java
import java.util.concurrent.*;

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(4);
        Future<Integer> future = pool.submit(() -> {
            Thread.sleep(1000);
            return 42;
        });

        System.out.println("主线程继续做别的...");
        Integer result = future.get();  // 阻塞等结果
        System.out.println("结果: " + result);
        pool.shutdown();
    }
}

语言: Java 5+ 运行: 编译运行 预期输出:

主线程继续做别的...
结果: 42

Future.get() 阻塞等待结果,就像 Thread.join() 但能拿返回值。任务抛异常时,get() 会抛出 ExecutionException

ThreadPoolExecutor:精确控制

Executors 底层调的就是 ThreadPoolExecutor。直接用它,你能精确控制每个参数:

java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2,              // 核心线程数
    5,              // 最大线程数
    60,             // 空闲线程存活时间
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),  // 有界队列——避免 OOM
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

语言: Java 5+

线程池的工作流程:

任务 → 核心线程有空闲? → 直接干
       ↓ 没空闲
       队列没满? → 排队
       ↓ 满了
       总线程 < maximum? → 开新线程
       ↓ 满了
       执行拒绝策略

四种拒绝策略:AbortPolicy(抛异常,默认)、CallerRunsPolicy(谁提交谁干)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃最老任务)。

生产环境最佳实践

  • 避免 Executors.newFixedThreadPool——它用无界队列,任务堆积可能 OOM。用 ThreadPoolExecutor 加有界队列。
  • 线程数估算:CPU 密集型 = CPU 核数 + 1;IO 密集型 = CPU 核数 * 2(经验值,需压测)。
  • 务必 shutdown()shutdown() 优雅关闭,shutdownNow() 发中断信号。
  • submit() vs execute():前者返回 Future 包装异常;后者不返回结果,异常由未捕获异常处理器处理。

ForkJoinPool:分治任务

ForkJoinPool 专为可递归拆分的任务设计。日常用 parallelStream 即可:

java
int sum = IntStream.range(1, 1_000_001)
    .parallel()       // 底层 ForkJoinPool
    .sum();
System.out.println(sum);  // 500000500000

语言: Java 8+


常见陷阱

  • 线程池里抛异常不会被外层捕获——submit() 的异常在 future.get() 时才抛出。
  • 忘了 shutdown() 会导致程序不退出。用 try { ... } finally { pool.shutdown(); } 包住。

旅人笔记

线程池提前招好工人,任务来了直接干。Executors 快捷方便但有坑。生产环境用 ThreadPoolExecutor 精确控制核心参数。Future.get() 阻塞拿结果。务必 shutdown() 优雅关闭。

下一步:I/O 与文件

程序不能只活在内存里。下一章看看怎么读写文件——I/O 操作也是并发场景最常见的搭档。

看 I/O 与文件 →

Built with VitePress | Software Systems Atlas