线程池概述

在多线程编程中,创建和销毁线程是一项相对昂贵的操作。为了减少频繁创建和销毁线程带来的性能开销,线程池(Thread Pool)技术应运而生。线程池通过提前创建一定数量的线程来 复用线程,提高系统的 效率性能

线程池的主要优势

  1. 避免频繁创建和销毁线程的开销
  2. 可以控制线程的最大并发数,防止资源过度消耗。
  3. 线程池可以更好地管理线程生命周期,避免线程泄漏。

Java 中的线程池

Java 中,线程池的使用通过 java.util.concurrent 包中的 Executor 接口及其实现类来完成。

1. 线程池核心接口:Executor

  • Executor 接口定义了执行异步任务的核心方法:execute(Runnable command)
  • ExecutorServiceExecutor 的子接口,提供了更多的线程池控制方法,如提交任务、关闭线程池等。

2. 常见线程池实现:

  1. ThreadPoolExecutor
    • 这是 ExecutorService 的最常用实现类,具有高度的自定义性。
  2. Executors 工厂类
    • 提供了几种常用的线程池实现,如 固定大小线程池单线程池可缓存线程池 等。

线程池的类型

1. 可缓存线程池(newCachedThreadPool()

  • 特点:线程池的大小会根据实际情况调整,如果线程池中没有空闲线程,就会创建新线程,空闲线程会在60秒后被回收。
  • 适用场景:适合执行大量短时间的小任务。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();

for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
}

executor.shutdown();
}
}
  • 解析newCachedThreadPool 创建一个可缓存线程池,根据任务量自动调整线程池的大小。

2. 固定大小线程池(newFixedThreadPool()

  • 特点:线程池中的线程数固定,如果所有线程都处于忙碌状态,新的任务会被放入队列等待。
  • 适用场景:适用于负载较重的任务,需要控制最大线程数的场景。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3); // 创建一个固定大小的线程池

for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
}

executor.shutdown();
}
}
  • 解析newFixedThreadPool(3) 创建一个包含 3 个线程的线程池,超过 3 个任务会被放入队列中等待。

3. 单线程池(newSingleThreadExecutor()

  • 特点:线程池只会创建一个线程,所有任务都会在该线程中顺序执行,适用于任务需要串行执行的场景。
  • 适用场景:适用于需要确保任务顺序执行的场景。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建一个单线程池

for (int i = 0; i < 5; i++) {
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
}

executor.shutdown();
}
}
  • 解析newSingleThreadExecutor 创建一个单线程池,所有任务会按顺序执行,不会并发。

4. 定时线程池(newScheduledThreadPool()

  • 特点:线程池支持定时任务执行,可以周期性地执行任务。
  • 适用场景:适合定时调度任务或周期性任务。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

executor.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getName() + " 执行定时任务");
}, 0, 2, TimeUnit.SECONDS); // 从 0 秒后每 2 秒执行一次

// 等待 10 秒后关闭线程池
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}

executor.shutdown();
}
}
  • 解析scheduleAtFixedRate 用于按固定频率执行任务,适合定时调度任务。

ThreadPoolExecutor 自定义线程池

1️⃣ 概述

ThreadPoolExecutor 是 Java 最强大的线程池实现类,允许我们自定义 核心线程数最大线程数任务队列拒绝策略 等参数,从而精细控制线程池的行为。

在实际开发中,合理使用线程池可以提高系统吞吐量降低资源消耗防止线程无限创建导致 OOM(内存溢出)。


2️⃣ ThreadPoolExecutor 构造方法

完整的 ThreadPoolExecutor 构造方法包含 7 个参数

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数 作用
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 非核心线程的存活时间
unit 时间单位
workQueue 任务队列(存放等待执行的任务)
threadFactory 线程工厂(可自定义线程名称等)
handler 拒绝策略(线程池满时如何处理新任务)

3️⃣ 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.*;

public class CustomThreadPoolExample {
public static void main(String[] args) {
// 1. 创建自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(2), // 任务队列(最多 2 个任务)
Executors.defaultThreadFactory(), // 线程工厂(默认)
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略(直接抛出异常)
);

// 2. 提交任务
for (int i = 0; i < 6; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
try {
Thread.sleep(1000); // 模拟任务执行
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 3. 关闭线程池
executor.shutdown();
}
}

4️⃣ 运行分析

🔹 线程池核心线程数 = 2,最大线程数 = 4,任务队列最多存 2 个任务:

  1. 前 2 个任务 直接由核心线程执行。
  2. 第 3、4 个任务 进入任务队列等待执行。
  3. 第 5、6 个任务 触发扩容,由最大线程数中的 2 个额外线程执行。
  4. 第 7 个任务 由于线程池 + 任务队列已满,根据 AbortPolicy 抛出异常

5️⃣ 线程池拒绝策略(RejectedExecutionHandler

策略 描述
AbortPolicy(默认) 直接抛出 RejectedExecutionException任务被丢弃 🚨
CallerRunsPolicy 由提交任务的线程(主线程)执行该任务,防止任务丢失
DiscardPolicy 直接丢弃无法执行的任务,不抛异常
DiscardOldestPolicy 丢弃任务队列中最老的任务,然后重新尝试提交新任务 🔄

📌 示例:使用 CallerRunsPolicy(防止任务丢失)

1
2
3
4
5
6
new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);

🔹 适用场景:希望任务不丢失,但可能会影响主线程性能。


6️⃣ 线程池使用注意事项

合理设置 corePoolSizemaximumPoolSize

  • CPU 密集型任务(计算型):corePoolSize = CPU 核心数
  • IO 密集型任务(磁盘/网络):corePoolSize = 2 * CPU 核心数

选择合适的 BlockingQueue

队列类型 特点
LinkedBlockingQueue 无界队列,可能导致 OOM
ArrayBlockingQueue 有界队列,需要指定大小
SynchronousQueue 不存储任务,提交任务必须有可用线程执行

合理使用 RejectedExecutionHandler

  • 关键任务(不能丢失):CallerRunsPolicy
  • 非关键任务(允许丢弃):DiscardPolicy

总结

线程池类型 特点 适用场景
可缓存线程池 (newCachedThreadPool()) 根据需求动态增加线程,空闲线程会被回收 适合大量短时间任务
固定线程池 (newFixedThreadPool()) 线程数固定,多余的任务会在队列中等待 适合负载较重,控制线程数的场景
单线程池 (newSingleThreadExecutor()) 只有一个线程,任务顺序执行 需要保证任务顺序执行的场景
定时线程池 (newScheduledThreadPool()) 支持定时任务执行 定时任务、周期性任务
自定义线程池 (ThreadPoolExecutor) 高度可定制,灵活配置核心线程数、最大线程数等 需要精细控制线程池行为的场景

总结:选择线程池时,要根据任务的特性、负载要求和并发需求,来决定使用哪种类型的线程池。