1 线程池概述
1.1 线程池简介
线程池(Thread Pool)是一种线程使用模式。在多线程编程中,线程的创建和销毁会带来一定的开销,尤其是在处理大量短时间任务时,频繁的线程创建和销毁会导致调度开销增加,进而影响缓存局部性和整体性能。线程池通过维护一组线程,等待监督管理者分配可并发执行的任务,从而避免了频繁创建和销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
1.2 线程池的优势
线程池的主要工作是控制运行的线程数量。在处理任务时,线程池会将任务放入队列中,然后在线程创建后启动这些任务。如果线程数量超过了预设的最大数量,超出数量的线程会排队等候,直到有其他线程执行完毕,再从队列中取出任务来执行。
1.3 线程池特点
- 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以立即执行,而不需要等待线程的创建。
- 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
1.4 线程池架构
在 Java 中,线程池是通过 Executor
框架实现的。该框架中涉及以下几个核心类和接口:
- Executor:这是一个接口,定义了执行任务的基本方法
execute(Runnable command)
。 - Executors:这是一个工具类,提供了创建不同类型线程池的静态方法,如
newFixedThreadPool
、newCachedThreadPool
、newSingleThreadExecutor
等。 - ExecutorService:这是
Executor
的子接口,扩展了Executor
的功能,提供了管理线程池的方法,如submit
、shutdown
、shutdownNow
等。 - ThreadPoolExecutor:这是
ExecutorService
接口的实现类,提供了线程池的具体实现,可以自定义线程池的参数,如核心线程数、最大线程数、任务队列等。
2 线程池的使用方式
在 Java 中,线程池的使用可以通过 Executors
工具类提供的静态方法来创建不同类型的线程池。以下是几种常见的线程池使用方式:
2.1 一池N线程
Executors.newFixedThreadPool(int nThreads)
- 描述:创建一个固定大小的线程池,线程池中的线程数量固定为
nThreads
。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则任务会被放入任务队列中等待执行。 - 适用场景:适用于任务量比较稳定,且任务执行时间较长的场景。
public class ThreadPoolDemo1 {public static void main(String[] args) {// 一池五线程 5个窗口ExecutorService threadPool1 = Executors.newFixedThreadPool(5);// 10个顾客请求try {for (int i = 1; i <= 10; i++) {// 执行threadPool1.execute(()->{System.out.println(Thread.currentThread().getName()+ " 办理业务");});}} catch (Exception e) {throw new RuntimeException(e);} finally {// 关闭threadPool1.shutdown();}}
}
2.2 一个任务一个任务执行,一池一线程
Executors.newSingleThreadExecutor()
- 描述:创建一个只有一个线程的线程池。所有任务都会按照提交的顺序依次执行,保证任务的执行顺序。
- 适用场景:适用于需要保证任务顺序执行的场景,如日志记录、文件操作等。
public class ThreadPoolDemo1 {public static void main(String[] args) {// 一池一线程 1个窗口ExecutorService threadPool2= Executors.newSingleThreadExecutor();// 10个顾客请求try {for (int i = 1; i <= 10; i++) {// 执行threadPool2.execute(()->{System.out.println(Thread.currentThread().getName()+ " 办理业务");});}} catch (Exception e) {throw new RuntimeException(e);} finally {// 关闭threadPool2.shutdown();}}
}
2.3 线程池根据需求创建线程,可扩容,遇强则强
Executors.newCachedThreadPool()
- 描述:创建一个可缓存的线程池。线程池会根据任务的数量动态调整线程的数量,如果当前没有空闲线程,则会创建新的线程;如果线程空闲时间超过60秒,则会被回收。
- 适用场景:适用于任务量不稳定,且任务执行时间较短的场景。
public class ThreadPoolDemo1 {public static void main(String[] args) {// 一池可扩容线程ExecutorService threadPool3 = Executors.newCachedThreadPool();// 10个顾客请求try {for (int i = 1; i <= 10; i++) {// 执行threadPool3.execute(()->{System.out.println(Thread.currentThread().getName()+ " 办理业务");});}} catch (Exception e) {throw new RuntimeException(e);} finally {// 关闭threadPool3.shutdown();}}
}
2.4 定时以及周期性执行任务
Executors.newScheduledThreadPool(int corePoolSize)
- 描述:创建一个可以定时以及周期性执行任务的线程池。线程池中的线程数量固定为
corePoolSize
,可以延迟执行任务或按照固定周期执行任务。 - 适用场景:适用于需要定时或周期性执行任务的场景,如定时任务调度、周期性数据同步等。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
scheduledThreadPool.scheduleAtFixedRate(() -> {System.out.println(Thread.currentThread().getName() + " is running");
}, 0, 1, TimeUnit.SECONDS); // 每秒执行一次
2.5 拥有多个任务队列
Executors.newWorkStealingPool()
- 描述:jdk1.8 提供的线程池,底层使用的是 ForkJoinPool 实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用 cpu 核数的线程来并行执行任务。
- 适用场景:适用于任务量较大且任务执行时间不均匀的场景,可以提高任务的并行度和执行效率。
ExecutorService workStealingPool = Executors.newWorkStealingPool();
for (int i = 0; i < 10; i++) {workStealingPool.execute(() -> {System.out.println(Thread.currentThread().getName() + " is running");});
}
workStealingPool.shutdown();
2.6 小结
newFixedThreadPool
:适用于任务量稳定,且任务执行时间较长的场景。newSingleThreadExecutor
:适用于需要保证任务顺序执行的场景。newCachedThreadPool
:适用于任务量不稳定,且任务执行时间较短的场景。newScheduledThreadPool
:适用于需要定时或周期性执行任务的场景。newWorkStealingPool
:适用于任务量较大且任务执行时间不均匀的场景,可以提高任务的并行度和执行效率。
3 线程池底层原理
在 Java 中,Executors
工具类提供了几种常见的线程池创建方法,包括 newFixedThreadPool
、newSingleThreadExecutor
、newCachedThreadPool
、newScheduledThreadPool
和 newWorkStealingPool
。这些方法的底层实现都依赖于 ThreadPoolExecutor
类或其变体,只是它们的参数配置不同。
3.1 newFixedThreadPool
Executors.newFixedThreadPool(int nThreads)
实现原理:
- 核心线程数:
nThreads
- 最大线程数:
nThreads
- 任务队列:
LinkedBlockingQueue
(无界队列) - 线程存活时间:0(线程不会被回收)
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
特点:
- 线程池中的线程数量固定为
nThreads
。 - 任务队列是无界的
LinkedBlockingQueue
,可以容纳无限多的任务。 - 线程不会被回收,因为线程存活时间为0。
3.2 newSingleThreadExecutor
Executors.newSingleThreadExecutor()
实现原理:
- 核心线程数:1
- 最大线程数:1
- 任务队列:
LinkedBlockingQueue
(无界队列) - 线程存活时间:0(线程不会被回收)
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
特点:
- 线程池中只有一个线程。
- 任务队列是无界的
LinkedBlockingQueue
,可以容纳无限多的任务。 - 线程不会被回收,因为线程存活时间为0。
- 任务会按照提交的顺序依次执行,保证任务的执行顺序。
3.3 newCachedThreadPool
Executors.newCachedThreadPool()
实现原理:
- 核心线程数:0
- 最大线程数:
Integer.MAX_VALUE
(理论上可以创建无限多的线程) - 任务队列:
SynchronousQueue
(同步队列,不存储任务) - 线程存活时间:60秒(线程空闲时间超过60秒会被回收)
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
特点:
- 线程池中的线程数量不固定,根据任务的数量动态调整。
- 任务队列是
SynchronousQueue
,它不存储任务,而是直接将任务交给线程执行。 - 线程空闲时间超过60秒会被回收,以节省系统资源。
- 适用于任务量不稳定,且任务执行时间较短的场景。
3.4 newScheduledThreadPool
Executors.newScheduledThreadPool(int corePoolSize)
实现原理:
- 核心线程数:
corePoolSize
- 最大线程数:
Integer.MAX_VALUE
(理论上可以创建无限多的线程) - 任务队列:
DelayedWorkQueue
(延迟队列) - 线程存活时间:0(线程不会被回收)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {return new ScheduledThreadPoolExecutor(corePoolSize);
}public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
特点:
- 线程池中的线程数量固定为
corePoolSize
。 - 任务队列是
DelayedWorkQueue
,用于存储延迟执行的任务。 - 适用于需要定时或周期性执行任务的场景。
3.5 newWorkStealingPool
Executors.newWorkStealingPool()
实现原理:
- 核心线程数:取决于系统可用的处理器数量(
Runtime.getRuntime().availableProcessors()
) - 最大线程数:取决于系统可用的处理器数量
- 任务队列:
ForkJoinPool
的工作窃取队列 - 线程存活时间:60秒(线程空闲时间超过60秒会被回收)
public static ExecutorService newWorkStealingPool() {return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),ForkJoinPool.defaultForkJoinWorkerThreadFactory,null, true);
}
特点:
- 线程池中的线程数量取决于系统可用的处理器数量。
- 任务队列是
ForkJoinPool
的工作窃取队列,每个线程都有一个私有的任务队列。 - 当一个线程的任务队列为空时,它会从其他线程的任务队列中窃取任务来执行。
- 适用于任务量较大且任务执行时间不均匀的场景,可以提高任务的并行度和执行效率。
4 线程池七个参数介绍
int corePoolSize
: 线程池的核心(常驻)线程数int maximumPoolSize
: 能容纳的最大线程数long keepAliveTime
:空闲线程存活时间TimeUnit unit
: 存活的时间单位BlockingQueue<Runnable> workQueue
:存放提交但未执行任务的队列ThreadFactory threadFactory
: 创建线程的工厂类RejectedExecutionHandler handler
: 等待队列满后的拒绝策略
5 线程池底层工作流程
线程池的工作流程可以分为任务提交、任务执行和线程回收三个主要阶段。以下是线程池的详细工作流程:
5.1 任务提交阶段
当调用 execute()
方法或 submit()
方法向线程池提交任务时,线程池会根据当前的状态和配置做出相应的判断和处理。
-
如果正在运行的线程数量小于
corePoolSize
:- 线程池会立即创建一个新的核心线程来运行这个任务。
-
如果正在运行的线程数量大于或等于
corePoolSize
:- 线程池会将这个任务放入任务队列(如
LinkedBlockingQueue
或SynchronousQueue
)中等待执行。
- 线程池会将这个任务放入任务队列(如
-
如果任务队列满了且正在运行的线程数量小于
maximumPoolSize
:- 线程池会创建一个新的非核心线程来立即运行这个任务。
-
如果任务队列满了且正在运行的线程数量大于或等于
maximumPoolSize
:- 线程池会启动饱和拒绝策略(如
AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
、DiscardOldestPolicy
)来处理这个任务。
- 线程池会启动饱和拒绝策略(如
5.2 任务执行阶段
当一个线程完成当前任务后,它会从任务队列中取下一个任务来执行。如果任务队列为空,线程会进入等待状态,直到有新的任务被提交。
5.3 线程回收阶段
当一个线程无事可做超过一定的时间(keepAliveTime
)时,线程池会进行线程回收。
-
如果当前运行的线程数大于
corePoolSize
:- 线程池会停掉这个线程,以减少资源消耗。
-
如果当前运行的线程数小于或等于
corePoolSize
:- 线程会继续保持活动状态,等待新的任务。
5.4 线程池的收缩
当线程池中的所有任务完成后,线程池会根据配置进行收缩。最终,线程池中的线程数量会收缩到 corePoolSize
的大小。
5.5 小结
线程池的工作流程可以概括为以下几个步骤:
- 任务提交:根据当前运行的线程数量和任务队列的状态,决定是创建新线程、将任务放入队列还是启动饱和拒绝策略。
- 任务执行:线程从任务队列中取任务并执行。
- 线程回收:当线程空闲时间超过
keepAliveTime
时,线程池会根据当前运行的线程数量决定是否回收线程。
6 拒绝策略
当线程池的任务队列已满且线程数量达到最大线程数时,线程池会触发拒绝策略来处理新提交的任务。Java 提供了四种内置的拒绝策略,分别是 AbortPolicy
、CallerRunsPolicy
、DiscardOldestPolicy
和 DiscardPolicy
。
6.1 AbortPolicy
- 描述:丢弃任务,并抛出
RejectedExecutionException
异常。这是线程池默认的拒绝策略。 - 适用场景:适用于需要严格控制任务提交的场景,确保任务不会被无条件丢弃。
- 注意事项:必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
public static class AbortPolicy implements RejectedExecutionHandler {public AbortPolicy() {}public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {throw new RejectedExecutionException("Task " + r.toString() +" rejected from " +e.toString());}
}
6.2 CallerRunsPolicy
- 描述:当触发拒绝策略时,只要线程池没有关闭,则使用调用线程直接运行任务。
- 适用场景:适用于并发较小、性能要求不高、不允许任务失败的场景。
- 注意事项:由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上会有较大损失。
public static class CallerRunsPolicy implements RejectedExecutionHandler {public CallerRunsPolicy() {}public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {r.run();}}
}
6.3 DiscardOldestPolicy
- 描述:当触发拒绝策略时,只要线程池没有关闭,丢弃阻塞队列
workQueue
中最老的一个任务,并将新任务加入。 - 适用场景:适用于任务队列中任务的优先级不重要,可以丢弃旧任务的场景。
- 注意事项:可能会导致一些任务被无条件丢弃,需要根据具体业务场景慎重选择。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {public DiscardOldestPolicy() {}public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {e.getQueue().poll();e.execute(r);}}
}
6.4 DiscardPolicy
- 描述:直接丢弃任务,不进行任何处理。
- 适用场景:适用于任务可以被无条件丢弃的场景,如日志记录等。
- 注意事项:任务被丢弃后不会有任何通知或异常抛出,需要确保业务逻辑允许任务丢失。
public static class DiscardPolicy implements RejectedExecutionHandler {public DiscardPolicy() {}public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}
}
6.5 小结
AbortPolicy
:丢弃任务并抛出异常,适用于需要严格控制任务提交的场景。CallerRunsPolicy
:使用调用线程直接运行任务,适用于并发较小、性能要求不高、不允许任务失败的场景。DiscardOldestPolicy
:丢弃队列中最老的任务,适用于任务优先级不重要的场景。DiscardPolicy
:直接丢弃任务,适用于任务可以被无条件丢弃的场景。
7 自定义线程池
在实际开发中,建议避免使用 Executors
工具类创建线程池,而是通过 ThreadPoolExecutor
类来自定义线程池。这样可以更好地控制线程池的参数,避免潜在的内存溢出(OOM)问题。
7.1 为什么避免使用 Executors
创建线程池?
-
FixedThreadPool
和SingleThreadPool
:- 允许的请求队列长度为
Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致内存溢出(OOM)。
- 允许的请求队列长度为
-
CachedThreadPool
和ScheduledThreadPool
:- 允许的创建线程数量为
Integer.MAX_VALUE
,可能会创建大量的线程,从而导致内存溢出(OOM)。
- 允许的创建线程数量为
7.2 自定义线程池的步骤
-
定义线程池的核心参数:
corePoolSize
:核心线程数。maximumPoolSize
:最大线程数。keepAliveTime
:线程空闲时间。unit
:时间单位。workQueue
:任务队列。threadFactory
:线程工厂。handler
:拒绝策略。
-
创建
ThreadPoolExecutor
实例:- 使用上述参数创建
ThreadPoolExecutor
实例。
- 使用上述参数创建
-
提交任务:
- 使用
execute()
或submit()
方法提交任务。
- 使用
7.3 示例代码
- 代码1
import java.util.concurrent.*;public class CustomThreadPool {public static void main(String[] args) {// 定义线程池的核心参数int corePoolSize = 5;int maximumPoolSize = 10;long keepAliveTime = 60L;TimeUnit unit = TimeUnit.SECONDS;BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);ThreadFactory threadFactory = Executors.defaultThreadFactory();RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();// 创建 ThreadPoolExecutor 实例ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);// 提交任务for (int i = 0; i < 150; i++) {threadPoolExecutor.execute(() -> {System.out.println(Thread.currentThread().getName() + " is running");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});}// 关闭线程池threadPoolExecutor.shutdown();}
}
-
参数解释
corePoolSize
:核心线程数,线程池中始终保持的线程数量。maximumPoolSize
:最大线程数,线程池中允许的最大线程数量。keepAliveTime
:线程空闲时间,当线程数量超过核心线程数时,多余的线程在空闲时间超过keepAliveTime
后会被回收。unit
:时间单位,keepAliveTime
的时间单位。workQueue
:任务队列,用于存放待执行的任务。这里使用LinkedBlockingQueue
,并设置队列长度为100。threadFactory
:线程工厂,用于创建新线程。这里使用默认的线程工厂。handler
:拒绝策略,当任务队列已满且线程数量达到最大线程数时,新任务的处理策略。这里使用AbortPolicy
,即丢弃任务并抛出异常。
-
代码2
public class ThreadPoolDemo2 {public static void main(String[] args) {ExecutorService threadPool = new ThreadPoolExecutor(2,5,2L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());// 10个顾客请求try {for (int i = 1; i <= 10; i++) {// 执行threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+ " 办理业务");});}} catch (Exception e) {throw new RuntimeException(e);} finally {// 关闭threadPool.shutdown();}}
}
8 思维导图
9 参考链接
【【尚硅谷】大厂必备技术之JUC并发编程】