线程池与 ExecutorService | JavaSE

线程池与 ExecutorService

一、学习目标

完成本章后,你应该能够:

  • 解释为什么频繁创建和销毁线程会带来性能和资源问题。
  • 理解线程池(Thread Pool)的核心思想:线程复用 + 任务管理 + 资源控制
  • 理清 ExecutorExecutorServiceExecutorsThreadPoolExecutor 之间的关系。
  • 使用 ExecutorService 执行 Runnable 任务。
  • 使用 ExecutorService 提交 Callable 任务,并通过 Future 获取结果。
  • 区分 execute()submit() 的主要差异。
  • 使用 Executors 创建常见线程池,并理解不同工厂方法的基本特点。
  • 正确使用 shutdown()shutdownNow() 管理线程池生命周期。
  • 理解为什么工程开发中不能只会调用 Executors.newFixedThreadPool(),还必须继续学习下一章的线程池参数配置。

二、核心知识

2.1 为什么需要线程池

前面创建线程时,我们通常会写:

Runnable task = new MyRunnable();

Thread thread = new Thread(task);

thread.start();

如果只有几个线程,这种方式没有问题。

但假设一个服务器不断收到用户请求:

请求1 → 创建线程1
请求2 → 创建线程2
请求3 → 创建线程3
请求4 → 创建线程4
……

如果来了:

10 个请求

创建 10 个线程可能还能接受。

如果同时来了:

10000 个请求

难道就直接:

创建 10000 个线程?

这就可能产生严重问题。

线程并不是免费的。

创建和维护线程需要消耗:

  • JVM 与操作系统资源
  • 线程栈内存
  • 调度资源
  • CPU 上下文切换成本
  • 创建与销毁线程的时间

如果线程数量无限增长:

任务越来越多
    ↓
线程越来越多
    ↓
内存占用增加
    ↓
线程调度和上下文切换增加
    ↓
CPU 大量时间花在线程切换上
    ↓
真正执行业务代码的时间反而下降

甚至可能最终导致:

系统资源耗尽
程序性能急剧下降
甚至无法继续创建线程

所以我们需要:

不要每来一个任务就临时创建一个线程,而是提前准备一批可以重复使用的线程。

这就是线程池。


2.2 什么是线程池

线程池(Thread Pool)可以理解为:

一个管理并重复利用工作线程的组件。

例如把线程池想象成一家饭店。

没有线程池:

来一个顾客
    ↓
临时招聘一个服务员
    ↓
服务完成
    ↓
把服务员辞退

下一个顾客来了
    ↓
重新招聘

这显然很浪费。

线程池:

          任务1
          任务2
          任务3
            ↓
      ┌───────────┐
      │   线程池   │
      │           │
      │ 线程1      │
      │ 线程2      │
      │ 线程3      │
      └───────────┘
            ↓
        执行任务
            ↓
       线程继续留下
            ↓
       等待下一个任务

所以线程池最核心的思想就是:

任务执行完
≠
线程销毁

而是:

任务执行完
    ↓
线程回到池中
    ↓
继续等待新任务

因此线程可以不断复用。


2.3 线程池管理的是线程,程序提交的是任务

这是理解线程池最关键的一点。

以前:

new Thread(task).start();

程序员同时负责:

创建线程
+
提交任务
+
启动线程

使用线程池以后:

pool.execute(task);

程序员主要负责:

提交任务

至于:

到底使用哪个线程?
什么时候创建线程?
线程什么时候复用?
任务需要等待吗?
线程什么时候销毁?

由线程池负责。

因此:

线程池把“任务是什么”和“任务由哪个线程执行”分离开了。

这也是 Java 并发包非常重要的设计思想。


2.4 Executor:任务执行的抽象

Java 从 JDK 5 开始提供了 java.util.concurrent 并发包。

其中有一个非常基础的接口:

Executor

它最核心的方法:

void execute(Runnable command);

例如:

Executor executor = ...;

executor.execute(task);

含义就是:

把一个 Runnable 任务交给执行器执行。

注意这个设计。

以前:

Thread thread = new Thread(task);
thread.start();

现在:

executor.execute(task);

调用者已经不关心:

具体使用哪个线程

而只关心:

我要执行这个任务

所以 Executor 的核心价值是:

将任务提交与具体线程执行机制解耦。


2.5 ExecutorService 是什么

实际开发中更常使用:

ExecutorService

它是一个接口:

Executor
   ↑
ExecutorService

可以简单理解为:

Executor
    ↓
只解决“执行任务”

ExecutorService
    ↓
在执行任务基础上
+
任务结果
+
线程池生命周期
+
批量任务管理

所以:

ExecutorService 是 Java 中非常重要的任务执行与线程池管理接口。

常见声明:

ExecutorService pool;

注意:

ExecutorService

是接口,不是直接 new

// 错误理解
new ExecutorService();

真正工作的通常是它的实现类。

其中非常重要的实现类:

ThreadPoolExecutor

关系可以先建立成:

Executor
    ↑
ExecutorService
    ↑
AbstractExecutorService
    ↑
ThreadPoolExecutor

当前阶段最重要的是前两层:

Executor
    ↓
基本任务执行能力

ExecutorService
    ↓
任务执行 + Future + 生命周期管理

至于 ThreadPoolExecutor 的完整内部运行机制,会在下一章专门学习。


2.6 ExecutorService、ThreadPoolExecutor、Executors 的关系

这三个名字特别容易混淆。

先看:

ExecutorService
ThreadPoolExecutor
Executors

它们分别是什么?

| 名称 | 类型 | 主要作用 | | -------------------- | ------ | --------------------------------------- | | ExecutorService | 接口 | 定义线程池任务执行和生命周期管理能力 | | ThreadPoolExecutor | 类 | ExecutorService 的重要线程池实现 | | Executors | 工具类 | 提供创建常见 ExecutorService 的工厂方法 |

可以类比:

List
ArrayList
Collections

虽然不完全一样,但可以辅助理解:

ExecutorService
≈
定义规范

ThreadPoolExecutor
≈
具体实现

Executors
≈
帮助创建对象的工具类

最重要的一句话:

Executors 不是线程池本身,它是用于创建和辅助操作 Executor 的工具类。


三、使用方法

3.1 使用 Executors 创建固定大小线程池

最简单的入门方式之一:

ExecutorService pool =
        Executors.newFixedThreadPool(3);

表示创建:

固定 3 个工作线程

的线程池。

例如提交 10 个任务:

任务1
任务2
任务3
任务4
……
任务10

线程池只有:

线程1
线程2
线程3

所以不会:

10 个任务 → 创建 10 个线程

而是类似:

线程1 → 任务1 → 任务4 → 任务7 → ...
线程2 → 任务2 → 任务5 → 任务8 → ...
线程3 → 任务3 → 任务6 → 任务9 → ...

实际调度顺序由 JVM 和操作系统决定,并不能保证一定按照上面的顺序。


3.2 使用 execute() 执行 Runnable

假设存在任务:

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        String name = Thread.currentThread().getName();

        System.out.println(
                name + " 正在执行任务"
        );
    }
}

使用线程池:

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

public class ExecutorServiceDemo {

    public static void main(String[] args) {

        ExecutorService pool =
                Executors.newFixedThreadPool(3);

        Runnable task = new MyRunnable();

        pool.execute(task);
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);

        pool.shutdown();
    }
}

这里提交了:

5 个任务

但线程池只有:

3 个线程

所以线程会被重复利用。

可能看到类似:

pool-1-thread-1 正在执行任务
pool-1-thread-2 正在执行任务
pool-1-thread-3 正在执行任务
pool-1-thread-1 正在执行任务
pool-1-thread-2 正在执行任务

这正体现了:

任务数量
≠
线程数量

任务可以很多,但是线程能够反复执行不同任务。


3.3 一个 Runnable 对象能不能提交多次

可以。

例如:

Runnable task = new MyRunnable();

pool.execute(task);
pool.execute(task);
pool.execute(task);

表示:

将这个任务对象提交三次。

线程池会把它当作三个任务执行请求处理。

但是要特别注意:

如果 Runnable 对象内部存在:

可变成员变量

并且多个线程会同时访问它,就可能重新涉及前面学习过的:

线程安全问题

所以:

使用线程池并不会自动解决线程安全问题。

线程池解决的是:

线程创建
线程复用
任务调度
资源管理

而:

共享数据竞争

仍然需要根据情况使用:

synchronized
Lock
线程安全容器
原子类
其他并发机制

3.4 execute() 的特点

核心方法:

void execute(Runnable command);

特点:

接收 Runnable
    ↓
提交任务
    ↓
没有直接返回任务结果

所以:

pool.execute(task);

非常适合:

只需要执行
不需要返回计算结果

的任务。

例如:

写日志
发送通知
处理连接
执行后台操作

3.5 使用 submit() 执行 Runnable

ExecutorService 还提供:

Future<?> submit(Runnable task);

例如:

Future<?> future = pool.submit(() -> {

    System.out.println(
            Thread.currentThread().getName()
                    + " 执行任务"
    );
});

此时虽然 Runnable

本身没有返回值

但:

submit()

仍然会返回:

Future<?>

如果任务正常结束:

future.get();

得到的结果通常是:

null

因为 Runnable 本身没有返回值。


3.6 Callable + submit()

前面已经学习过:

Callable<V>

它与 Runnable 最大的区别之一:

Runnable
    ↓
run()
    ↓
没有返回值

Callable<V>
    ↓
call()
    ↓
可以返回 V 类型结果

线程池中处理 Callable 时:

Future<T> submit(Callable<T> task);

例如:

import java.util.concurrent.Callable;

public class SumTask implements Callable<Integer> {

    private final int n;

    public SumTask(int n) {
        this.n = n;
    }

    @Override
    public Integer call() {

        int sum = 0;

        for (int i = 1; i <= n; i++) {
            sum += i;
        }

        return sum;
    }
}

提交:

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

public class CallablePoolDemo {

    public static void main(String[] args)
            throws Exception {

        ExecutorService pool =
                Executors.newFixedThreadPool(3);

        Future<Integer> future1 =
                pool.submit(new SumTask(100));

        Future<Integer> future2 =
                pool.submit(new SumTask(200));

        System.out.println(future1.get());
        System.out.println(future2.get());

        pool.shutdown();
    }
}

这里完整链路是:

Callable<Integer>
        ↓
pool.submit(...)
        ↓
Future<Integer>
        ↓
future.get()
        ↓
Integer 结果

3.7 Future 在这里到底是什么

可以把 Future 理解成:

异步任务未来结果的凭证。

提交任务时:

Future<Integer> future =
        pool.submit(task);

这个时候任务可能:

还没开始
正在执行
已经完成

但是调用者已经先拿到了:

Future

后面再:

Integer result = future.get();

获取结果。

可以想象:

你去奶茶店下单
       ↓
拿到取餐号
       ↓
奶茶店后台制作
       ↓
根据取餐号拿结果

其中:

Callable
≈
订单任务

线程池
≈
奶茶店后台工作人员

Future
≈
取餐凭证

Future.get()
≈
等待并取得结果

3.8 Future.get() 会不会等待

会。

如果:

future.get();

调用时任务还没有执行完:

当前线程
    ↓
等待
    ↓
任务完成
    ↓
获得结果

因此:

Future.get() 是一个可能发生阻塞的方法。

例如:

Future<String> future = pool.submit(() -> {

    Thread.sleep(3000);

    return "执行完成";
});

System.out.println("任务已经提交");

String result = future.get();

System.out.println(result);

可能表现为:

任务已经提交

等待约一段时间

执行完成

所以异步提交:

submit()

并不意味着:

整个程序永远不会等待

一旦立即调用:

get()

当前线程仍然可能等待任务完成。


3.9 execute() 和 submit() 的区别

这是本章必须掌握的重点。

| 对比 | execute() | submit() | | ------------ | ------------------ | ----------------- | | 所属接口 | Executor | ExecutorService | | Runnable | 支持 | 支持 | | Callable | 不直接支持 | 支持 | | 返回 Future | 不返回 | 返回 | | 获取任务结果 | 不可以直接获取 | 可以 | | 任务状态跟踪 | 较弱 | 可通过 Future | | 取消任务 | 无 Future 直接操作 | Future 可参与取消 |

简单选择:

只执行 Runnable
不关心结果
        ↓
execute()

如果:

需要返回结果
需要 Future
需要跟踪任务
        ↓
submit()

Callable 必须使用类似:

pool.submit(callable);

的方式提交。


3.10 Executors 常见工厂方法

Executors 提供了一些方便创建线程池的方法。


1. newFixedThreadPool()

ExecutorService pool =
        Executors.newFixedThreadPool(4);

特点:

使用固定数量的工作线程执行任务。

例如:

4 个工作线程
100 个任务

同一时刻最多由这些工作线程不断处理任务,其余任务等待执行。

适合帮助我们理解:

固定工作线程
+
任务排队
+
线程复用

2. newSingleThreadExecutor()

ExecutorService pool =
        Executors.newSingleThreadExecutor();

特点:

只有一个工作线程

因此任务会:

一个接一个执行

例如:

任务1
 ↓
任务2
 ↓
任务3
 ↓
任务4

适用于希望:

按照单线程方式串行执行任务

的场景。


3. newCachedThreadPool()

ExecutorService pool =
        Executors.newCachedThreadPool();

其特点是:

根据任务需要创建线程,并优先复用已经存在的空闲线程。

空闲线程经过一段时间后可以被回收。

这类线程池适合:

大量较短的异步任务

但必须注意:

高并发情况下,如果任务提交速度持续超过任务完成速度,线程数量可能增长得非常多。

所以不能因为:

写起来简单

就在不了解运行机制的情况下用于所有业务。


4. newScheduledThreadPool()

ScheduledExecutorService pool =
        Executors.newScheduledThreadPool(3);

它主要用于:

延迟执行
周期执行

例如:

5 秒之后执行一次任务

或者:

每隔一段时间执行任务

它返回的是更具体的接口:

ScheduledExecutorService

属于线程池体系的进一步能力。


3.11 为什么不能只会 Executors

Executors 最大的优点:

简单
方便
代码少

例如:

Executors.newFixedThreadPool(4);

一行代码就能创建线程池。

但是工程开发不能只停留在:

“能创建出来”

还必须知道:

最多允许多少线程?
任务来不及处理怎么办?
任务在哪里等待?
可以等待多少任务?
什么时候创建更多线程?
什么时候拒绝任务?

这些问题决定:

吞吐量
响应时间
内存占用
系统稳定性

所以本章先掌握:

如何使用线程池

下一章继续学习:

线程池到底如何配置

也就是:

ThreadPoolExecutor

的完整运行规则。


3.12 shutdown():有序关闭线程池

线程池使用完成后应该关闭。

常见:

pool.shutdown();

它的含义不是:

立刻杀死所有线程

而是:

不再接收新的任务
        ↓
已经提交的任务继续执行
        ↓
这些任务全部结束
        ↓
线程池最终终止

例如:

ExecutorService pool =
        Executors.newFixedThreadPool(2);

pool.execute(task1);
pool.execute(task2);
pool.execute(task3);

pool.shutdown();

shutdown() 之后:

task1
task2
task3

仍然可以继续完成。

但是此后再:

pool.execute(task4);

线程池已经进入关闭流程,新任务将不再被正常接受。


3.13 shutdown() 会不会等任务执行完再返回

这是一个非常容易误解的地方。

pool.shutdown();

表示:

发出有序关闭请求。

但是:

shutdown()

这个方法本身:

不会阻塞当前线程一直等待所有任务完成。

也就是说:

pool.shutdown();

System.out.println("main继续执行");

主线程可以继续向下执行。

如果确实需要:

等待线程池真正终止

可以配合:

awaitTermination()

例如:

pool.shutdown();

boolean terminated =
        pool.awaitTermination(
                10,
                TimeUnit.SECONDS
        );

System.out.println(
        "是否已结束:" + terminated
);

3.14 shutdownNow():尝试立即停止

还有:

List<Runnable> tasks =
        pool.shutdownNow();

它的目标是:

停止接收新任务
+
不再启动仍在等待的任务
+
尝试中断正在执行的任务

但必须特别注意:

shutdownNow() 并不能保证已经运行的任务立即结束。

因为 Java 通常通过:

Thread.interrupt()

发出中断请求。

如果正在运行的任务:

完全不响应中断

那么任务仍然可能继续运行。

所以不要把:

shutdownNow()

理解为操作系统意义上的:

强制杀死所有线程

更加准确的理解是:

尽最大努力停止。

它的返回值:

List<Runnable>

表示:

那些已经提交,但尚未真正开始执行的任务。


四、原理与进阶

4.1 Executor 框架真正改变了什么

以前:

new Thread(task).start();

代码同时绑定:

任务
+
线程

Executor 模型:

任务
    ↓
Executor
    ↓
具体执行策略

因此我们可以把:

Runnable
Callable

看成:

“要干什么”

把:

ExecutorService

看成:

“谁来安排怎么干”

这是一种典型的:

任务与执行机制解耦。


4.2 线程池为什么能够提高效率

假设处理:

10000 个短任务

方案一:

任务1 → 创建线程 → 执行 → 销毁
任务2 → 创建线程 → 执行 → 销毁
任务3 → 创建线程 → 执行 → 销毁
……

大量成本消耗在:

创建线程
销毁线程

方案二:

先创建少量工作线程
        ↓
不断从任务中取工作
        ↓
任务完成后继续等待
        ↓
重复利用

线程池避免了大量不必要的线程重复创建。

同时还能:

限制并发线程规模
统一管理任务
统一关闭
控制系统资源

因此线程池的价值不仅仅是:

“快一点”

而是:

让并发任务执行变得更加可控。


4.3 线程越多是不是性能越高

不是。

假设 CPU 只有有限的计算能力。

线程从:

4

变成:

40000

并不意味着性能变成:

10000 倍

反而可能出现:

大量线程
    ↓
大量上下文切换
    ↓
线程竞争 CPU
    ↓
内存消耗增加
    ↓
整体性能下降

因此:

线程池的重要目标之一,就是控制线程数量,而不是无限创建线程。

线程数究竟配置多少,与:

CPU 密集型任务
IO 密集型任务
机器 CPU 核心数
任务耗时
阻塞比例
系统资源

都有关系。

本阶段先掌握这个原则:

线程数量不是越多越好。


4.4 JDK 21 中 ExecutorService 的资源管理

在 JDK 21 中:

ExecutorService

同时属于:

AutoCloseable

因此可以使用:

try (ExecutorService pool =
             Executors.newFixedThreadPool(3)) {

    pool.submit(...);
    pool.submit(...);
}

离开 try 后会进行有序关闭。

这是 JDK 21 下可以使用的一种资源管理方式。

不过初学阶段仍然必须理解:

shutdown()
shutdownNow()
awaitTermination()

因为只有理解生命周期,才能真正理解线程池是如何结束的。


五、实践应用

5.1 模拟 20 个任务由 3 个线程执行

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

public class PoolTaskDemo {

    public static void main(String[] args) {

        ExecutorService pool =
                Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 20; i++) {

            int taskId = i;

            pool.execute(() -> {

                String threadName =
                        Thread.currentThread().getName();

                System.out.println(
                        threadName
                                + " 正在处理任务 "
                                + taskId
                );
            });
        }

        pool.shutdown();
    }
}

这里最值得观察的是:

任务一共有 20 个

但是线程名称反复出现:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3

这就证明:

20 个任务
        ↓
不是 20 个线程

而是
        ↓
3 个线程反复处理 20 个任务

这就是线程复用。


5.2 多个 Callable 并行计算

例如分别计算:

1 ~ 100
1 ~ 1000
1 ~ 10000

可以:

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

public class CalculateDemo {

    public static void main(String[] args)
            throws Exception {

        ExecutorService pool =
                Executors.newFixedThreadPool(3);

        Future<Long> f1 =
                pool.submit(() -> sum(100));

        Future<Long> f2 =
                pool.submit(() -> sum(1000));

        Future<Long> f3 =
                pool.submit(() -> sum(10000));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());

        pool.shutdown();
    }

    public static long sum(int n) {

        long result = 0;

        for (int i = 1; i <= n; i++) {
            result += i;
        }

        return result;
    }
}

完整模型:

任务1 ─┐
任务2 ─┼→ ExecutorService → 多个工作线程
任务3 ─┘
                            ↓
                         Future
                            ↓
                          get()
                            ↓
                           结果

5.3 为什么网络服务器尤其需要线程池

后面学习 TCP 多客户端服务器时,会出现:

客户端1连接
客户端2连接
客户端3连接
客户端4连接
……

最简单的方案可能是:

while (true) {

    Socket socket = serverSocket.accept();

    new Thread(() -> {
        handle(socket);
    }).start();
}

问题是:

连接越多
    ↓
线程越多

如果恶意或突发出现大量连接:

服务器线程数量可能快速增长

线程池可以改成:

Socket
  ↓
Runnable任务
  ↓
ExecutorService
  ↓
有限数量工作线程

因此:

线程池是后续多客户端网络服务器的重要基础。

09-09 会进一步学习:

使用线程池优化网络服务端

六、常见问题

6.1 线程池是不是一个线程

不是。

线程池:

Thread Pool

是:

管理多个工作线程和任务执行过程的组件。

里面通常可能存在多个线程。


6.2 一个任务对应一个线程吗

不是。

线程池的重要目的恰好就是避免:

一个任务
=
创建一个线程

一个线程可以:

执行任务1
执行完成
执行任务2
执行完成
执行任务3

不断复用。


6.3 ExecutorService 是类吗

不是。

它是:

interface ExecutorService

也就是接口。

常见具体实现包括:

ThreadPoolExecutor

6.4 Executors 是线程池吗

严格来说不是。

Executors

是工具类。

它可以:

Executors.newFixedThreadPool(...)

创建并返回:

ExecutorService

6.5 Runnable 可以用 submit() 吗

可以。

Future<?> future =
        pool.submit(runnable);

只是 Runnable 本身没有业务返回值,因此:

future.get()

正常完成时得到的是:

null

6.6 Callable 可以用 execute() 吗

不能直接:

pool.execute(callable);

因为:

execute()

接收的是:

Runnable

而 Callable 通常通过:

pool.submit(callable);

提交。


6.7 submit() 是不是一定有业务结果

不是。

submit(Runnable)

也会返回 Future。

但是 Runnable:

本身没有返回值

所以正常完成时:

Future.get()

返回:

null

真正典型的“有返回结果任务”是:

Callable<T>

6.8 调用 shutdown() 后已经提交的任务会消失吗

不会。

shutdown() 是:

停止接受新任务

但已经提交的任务仍然会执行。


6.9 shutdownNow() 是否一定能立刻结束所有线程

不能保证。

它会:

尝试中断正在执行的任务

但是任务是否真的结束,与任务是否正确响应:

interrupt()

有关。


6.10 shutdown() 后还能提交新任务吗

不能正常继续提交。

例如:

pool.shutdown();

pool.execute(task);

线程池已经进入关闭状态,新任务会被拒绝。


6.11 为什么程序明明执行完了却没有退出

一个非常常见的原因是:

ExecutorService pool =
        Executors.newFixedThreadPool(3);

使用完成后没有:

pool.shutdown();

线程池中的工作线程仍然存在,JVM 可能因此继续运行。

所以必须形成习惯:

创建线程池
    ↓
提交任务
    ↓
完成业务
    ↓
关闭线程池

6.12 newFixedThreadPool 是不是生产环境永远不能用

不能这样绝对理解。

Executors.newFixedThreadPool() 是 JDK 正式提供的 API,本身当然是合法、可用的线程池创建方式。

真正需要注意的是:

必须了解它背后的线程数量、任务队列和资源边界。

对于:

学习
简单工具
负载可控的场景

直接使用非常方便。

但是高并发核心业务如果完全不了解底层配置:

任务可能大量积压
资源可能超出预期

所以工程开发中通常需要进一步掌握:

ThreadPoolExecutor

并明确配置线程池运行规则。

这正是下一章的内容。


七、练习与验收

7.1 知识问答

  1. 为什么频繁创建和销毁线程会产生性能开销?
  2. 什么是线程池?
  3. 线程池最核心的三个价值是什么?
  4. 为什么说线程池实现了线程复用?
  5. 任务数量是否一定等于线程数量?
  6. Executor 解决的核心问题是什么?
  7. ExecutorServiceExecutor 是什么关系?
  8. ExecutorService 是类还是接口?
  9. ThreadPoolExecutorExecutorService 是什么关系?
  10. Executors 是什么?
  11. ExecutorsExecutorService 有什么区别?
  12. execute() 接收什么类型的任务?
  13. execute() 有没有返回 Future?
  14. submit() 可以提交哪些任务?
  15. submit(Callable<T>) 返回什么?
  16. Future 的主要作用是什么?
  17. Future.get() 是否可能阻塞?
  18. Runnable 能不能通过 submit() 执行?
  19. Callable 为什么通常使用 submit()
  20. newFixedThreadPool() 有什么特点?
  21. newSingleThreadExecutor() 有什么特点?
  22. newCachedThreadPool() 有什么基本特点?
  23. newScheduledThreadPool() 解决什么问题?
  24. 为什么线程数量不是越多越好?
  25. 为什么线程池仍然存在共享数据安全问题?
  26. shutdown() 的作用是什么?
  27. shutdown() 会不会等待任务全部完成后才返回?
  28. shutdownNow() 是否保证立即杀死所有线程?
  29. awaitTermination() 有什么作用?
  30. 为什么使用完线程池应该主动关闭?

7.2 代码阅读

阅读:

ExecutorService pool =
        Executors.newFixedThreadPool(2);

for (int i = 1; i <= 10; i++) {

    int id = i;

    pool.execute(() -> {

        System.out.println(
                Thread.currentThread().getName()
                        + ":" + id
        );
    });
}

pool.shutdown();

回答:

  1. 一共提交多少个任务?
  2. 线程池最多有多少个固定工作线程?
  3. 是否意味着会创建 10 个线程?
  4. 为什么同一个线程名称可能出现多次?
  5. 10 个任务的输出顺序是否一定是 1~10?
  6. shutdown() 是否会导致前面提交的任务直接消失?

阅读:

ExecutorService pool =
        Executors.newFixedThreadPool(2);

Future<Integer> future =
        pool.submit(() -> {

            Thread.sleep(2000);

            return 100;
        });

System.out.println("A");

System.out.println(future.get());

System.out.println("B");

pool.shutdown();

回答:

  1. submit() 调用以后是否必须等待任务执行完成才能得到 Future
  2. future.get() 是否可能阻塞?
  3. A100B 的逻辑输出顺序是什么?
  4. Callable 的返回值最终保存在哪里?

7.3 手写代码

任务一:固定大小线程池

创建:

3 个线程的固定线程池

提交:

20 个 Runnable 任务

每个任务输出:

任务编号
当前线程名称

观察:

20 个任务最终由几个线程反复执行?


任务二:Callable 求和

定义:

Callable<Long>

计算:

1 ~ n

分别提交:

n = 100
n = 1000
n = 10000

使用:

Future<Long>

获取三个结果。


任务三:单线程执行器

使用:

Executors.newSingleThreadExecutor()

依次提交:

任务A
任务B
任务C
任务D

每个任务休眠一小段时间。

观察:

是否始终只有一个线程在执行?


任务四:Future

提交一个 Callable:

休眠 2 秒
返回 "SUCCESS"

要求:

  1. 先输出 "任务已经提交"
  2. 再使用 Future 获取结果。
  3. 输出返回值。
  4. 说明程序在哪一步可能发生等待。

7.4 Debug

下面代码有什么问题:

public static void main(String[] args) {

    ExecutorService pool =
            Executors.newFixedThreadPool(3);

    for (int i = 0; i < 10; i++) {
        pool.execute(() -> {
            System.out.println("执行任务");
        });
    }
}

要求:

  1. 程序任务能否执行?
  2. 为什么程序可能迟迟不结束?
  3. 如何修改?

下面代码:

ExecutorService pool =
        Executors.newFixedThreadPool(2);

pool.shutdown();

pool.execute(() -> {
    System.out.println("Hello");
});

要求:

  1. 分析逻辑错误。
  2. shutdown() 后线程池处于什么状态?
  3. 为什么不能继续正常提交任务?

下面代码:

Callable<Integer> task = () -> 100;

pool.execute(task);

要求:

  1. 判断能否编译。
  2. execute() 需要什么任务类型?
  3. Callable 应该如何提交?
  4. 如何获取结果?

7.5 综合训练

设计一个:

多任务计算中心

系统需要同时完成:

任务1:计算 1~100000 的和
任务2:计算 1~10000 的偶数数量
任务3:寻找 1~10000 中能够被 7 整除的数字数量
任务4:模拟一个耗时查询并返回字符串结果

要求:

  1. 创建一个 ExecutorService
  2. 使用多个 Callable 表示不同任务。
  3. 使用 submit() 提交。
  4. 保存多个 Future
  5. 主线程可以在任务执行期间继续执行其他代码。
  6. 最终使用 Future.get() 获取所有结果。
  7. 程序结束前正确关闭线程池。
  8. 输出每个任务实际由哪个线程执行。

完成后回答:

如果不用线程池,而是每个任务都 new Thread(),两种方案在设计思想上有什么区别?


7.6 本章验收

如果你能够不查资料完成下面这些内容,本章即可认为基本掌握:

  • 能解释为什么需要线程池。
  • 能说明线程池为什么可以减少频繁创建线程的成本。
  • 能解释“任务”和“线程”不是同一个东西。
  • 能画出 Executor → ExecutorService → ThreadPoolExecutor 的基本关系。
  • 能解释 Executors 为什么只是工具类。
  • 能使用 newFixedThreadPool() 创建线程池。
  • 能使用 execute() 提交 Runnable。
  • 能使用 submit() 提交 Runnable。
  • 能使用 submit() 提交 Callable。
  • 能通过 Future 获取 Callable 的结果。
  • 能解释 Future.get() 为什么可能阻塞。
  • 能说清 execute()submit() 的主要区别。
  • 能解释 shutdown()shutdownNow() 的区别。
  • 能解释为什么线程数量不是越多越好。
  • 能解释为什么线程池不能自动解决线程安全问题。
  • 能说明为什么后续还需要继续学习 ThreadPoolExecutor

最终应形成下面这条知识链:

Runnable / Callable
        ↓
       任务
        ↓
Executor
        ↓
ExecutorService
        ↓
提交任务
        ↓
线程池中的工作线程
        ↓
重复执行不同任务
        ↓
Callable → Future → 结果
        ↓
shutdown()
        ↓
线程池生命周期结束

本章真正需要建立的核心思想不是背 API,而是:

应用程序负责描述和提交任务,ExecutorService 负责管理任务如何由线程执行。

下一章再继续解决:

线程池内部到底有多少线程?
任务满了放在哪里?
什么时候创建临时线程?
线程数量到达上限怎么办?
队列也满了怎么办?
为什么会拒绝任务?

也就是进入:

ThreadPoolExecutor
+
任务队列
+
拒绝策略

的完整运行机制。