线程池与 ExecutorService | JavaSE
线程池与 ExecutorService
一、学习目标
完成本章后,你应该能够:
- 解释为什么频繁创建和销毁线程会带来性能和资源问题。
- 理解线程池(Thread Pool)的核心思想:线程复用 + 任务管理 + 资源控制。
- 理清
Executor、ExecutorService、Executors、ThreadPoolExecutor之间的关系。 - 使用
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 知识问答
- 为什么频繁创建和销毁线程会产生性能开销?
- 什么是线程池?
- 线程池最核心的三个价值是什么?
- 为什么说线程池实现了线程复用?
- 任务数量是否一定等于线程数量?
Executor解决的核心问题是什么?ExecutorService与Executor是什么关系?ExecutorService是类还是接口?ThreadPoolExecutor与ExecutorService是什么关系?Executors是什么?Executors和ExecutorService有什么区别?execute()接收什么类型的任务?execute()有没有返回 Future?submit()可以提交哪些任务?submit(Callable<T>)返回什么?Future的主要作用是什么?Future.get()是否可能阻塞?- Runnable 能不能通过
submit()执行? - Callable 为什么通常使用
submit()? newFixedThreadPool()有什么特点?newSingleThreadExecutor()有什么特点?newCachedThreadPool()有什么基本特点?newScheduledThreadPool()解决什么问题?- 为什么线程数量不是越多越好?
- 为什么线程池仍然存在共享数据安全问题?
shutdown()的作用是什么?shutdown()会不会等待任务全部完成后才返回?shutdownNow()是否保证立即杀死所有线程?awaitTermination()有什么作用?- 为什么使用完线程池应该主动关闭?
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();
回答:
- 一共提交多少个任务?
- 线程池最多有多少个固定工作线程?
- 是否意味着会创建 10 个线程?
- 为什么同一个线程名称可能出现多次?
- 10 个任务的输出顺序是否一定是 1~10?
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();
回答:
submit()调用以后是否必须等待任务执行完成才能得到Future?future.get()是否可能阻塞?A、100、B的逻辑输出顺序是什么?- 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"
要求:
- 先输出
"任务已经提交"。 - 再使用 Future 获取结果。
- 输出返回值。
- 说明程序在哪一步可能发生等待。
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("执行任务");
});
}
}
要求:
- 程序任务能否执行?
- 为什么程序可能迟迟不结束?
- 如何修改?
下面代码:
ExecutorService pool =
Executors.newFixedThreadPool(2);
pool.shutdown();
pool.execute(() -> {
System.out.println("Hello");
});
要求:
- 分析逻辑错误。
shutdown()后线程池处于什么状态?- 为什么不能继续正常提交任务?
下面代码:
Callable<Integer> task = () -> 100;
pool.execute(task);
要求:
- 判断能否编译。
execute()需要什么任务类型?- Callable 应该如何提交?
- 如何获取结果?
7.5 综合训练
设计一个:
多任务计算中心
系统需要同时完成:
任务1:计算 1~100000 的和
任务2:计算 1~10000 的偶数数量
任务3:寻找 1~10000 中能够被 7 整除的数字数量
任务4:模拟一个耗时查询并返回字符串结果
要求:
- 创建一个
ExecutorService。 - 使用多个
Callable表示不同任务。 - 使用
submit()提交。 - 保存多个
Future。 - 主线程可以在任务执行期间继续执行其他代码。
- 最终使用
Future.get()获取所有结果。 - 程序结束前正确关闭线程池。
- 输出每个任务实际由哪个线程执行。
完成后回答:
如果不用线程池,而是每个任务都
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
+
任务队列
+
拒绝策略
的完整运行机制。