前言:线程池是 Java 中用于高效管理并发任务的工具,通过复用线程、降低线程创建销毁的开销,提升系统性能与响应速度。它帮助开发者更好地控制线程生命周期和资源消耗,是高并发应用的重要组成部分。
在正式开始讲解之前,先让我们看一下本文大致的讲解内容:
1.线程池的核心原理
(1)基本概念
在正式开始学习Java中的线程池之前,先让我们了解一下什么是线程池,以下为线程池的基本概念:
在并发编程中,线程池是一个重要的工具,它允许我们复用已经创建的线程,从而减少线程创建和销毁的开销。线程池可以有效地管理并发任务,避免系统因为创建过多线程而产生的性能瓶颈。
在了解完了线程池的基本概念之后,让我们看一下线程池的组成部分,线程池的基本结构通常由以下几个部分组成:
核心线程数 (
corePoolSize
):线程池中常驻的线程数。即使没有任务执行,线程池也会保持这些线程在空闲状态。最大线程数 (
maximumPoolSize
):线程池中允许的最大线程数。如果任务数超过线程池的核心线程数并且任务队列已满,线程池将会创建更多的线程,直到达到最大线程数。任务队列:用于存放提交到线程池的任务。不同类型的任务队列会影响线程池的性能。例如,
LinkedBlockingQueue
是一个无界队列,可以存放大量任务,而ArrayBlockingQueue
是一个有界队列,适合在任务数有上限的场景中使用。线程工厂 (
ThreadFactory
):用于创建新线程。你可以自定义线程工厂,以便为线程池中的线程命名或设置优先级等属性。拒绝策略 (
RejectedExecutionHandler
):当线程池无法接受新的任务时,可以采用不同的拒绝策略,比如丢弃任务或抛出异常。
读者在读完上述对线程池的组成部分的描述之后,可能还是不能理解线程池中的这些构成部分,不过没关系,随着我们对线程池的进一步讲解之后,读者就可以更好的理解这些部分了!
(2)线程池的工作流程
在了解完了线程池的基本概念之后,在让我们进一步了解一下Java中的线程池是如何工作的,其工作原理又是什么
线程池的基本工作流程可以分为以下几个步骤:
程可以分为以下几个步骤:
任务提交: 当一个任务被提交给线程池时,线程池首先会尝试将任务放入任务队列。如果任务队列有空间,任务就会排队等待执行。如果任务队列满了,且线程池中的线程数未达到最大线程数,则线程池会创建新的线程来处理任务。如果线程池中的线程数已经达到最大值,且任务队列也满了,线程池会根据配置的拒绝策略来处理任务。
任务执行: 线程池中的线程会从任务队列中获取任务并执行。执行完成后,线程并不会被销毁,而是返回到线程池中,准备接收下一个任务。
线程回收: 如果线程池中的线程长时间处于空闲状态,且空闲时间超过了设定的阈值,线程池会回收这些线程,以节省系统资源。回收的线程数不会低于核心线程数,只有当线程数大于核心线程数时,线程池才会销毁空闲线程。
拒绝策略触发: 如果线程池的任务队列已满,并且线程池中的线程数已经达到了最大线程数,再提交的任务就会被拒绝。这时,线程池会根据配置的拒绝策略来处理任务,如抛出异常、丢弃任务、丢弃队列中最老的任务,或者由提交任务的线程自己执行任务。
通过上述的讲解,我们就大致的了解了Java中的线程池是如何工作的了,这对于我们接下来的学习是至关重要的。
(3)线程池的关键组件的实现方式
在上文中我们已经了解了Java中的线程池的基本结构了,这里我们进一步讲解一下线程池的关键组件的实现方式。
线程池的组件包括核心线程数、最大线程数、任务队列、线程工厂和拒绝策略,这些组件的配置决定了线程池的行为与性能,下面是每个组件的详细介绍。
1. 核心线程数与最大线程数
核心线程数:线程池在没有任务时保持的最小线程数,避免了线程的频繁创建和销毁。通过合理设置
corePoolSize
,可以确保在任务负载较轻时,线程池仍然保持一定数量的线程,以便及时响应新的任务。最大线程数:线程池允许的最大线程数。当任务量剧增且任务队列已满时,线程池会根据
maximumPoolSize
来创建新的线程,但不会超过该最大值。
2. 任务队列
线程池的任务队列有不同的实现方式:
LinkedBlockingQueue
:一个无界队列,适用于任务量较大的场景,能容纳大量待处理的任务。若任务队列中有空位,线程池就会继续添加任务,不会立即创建新的线程。
ArrayBlockingQueue
:一个有界队列,适用于任务量较小且固定的场景。当任务队列已满时,线程池会尝试创建新的线程,直到达到最大线程数。
SynchronousQueue
:一个零容量队列,适合任务较为紧凑并且执行迅速的场景。每当一个任务到来时,必须有线程立即接收并执行这个任务。
3. 线程工厂
线程工厂用于创建线程。通过自定义线程工厂,开发者可以为线程指定特定的名称、优先级,或者让线程成为守护线程等。
4. 拒绝策略
线程池中的拒绝策略用于处理任务过载时的情况。常见的拒绝策略包括:
AbortPolicy
:默认策略,抛出RejectedExecutionException
异常。
DiscardPolicy
:丢弃当前任务。
DiscardOldestPolicy
:丢弃任务队列中最旧的任务。
CallerRunsPolicy
:让提交任务的线程来执行该任务。
至此,我们就大致的对Java中的线程池有了初步的理解了!!!
2.线程池的使用
在了解完了Java中的线程池的基本概念以及原理之后,现在让我们学习一下如何去使用Java中的线程池吧,不过首先我们需要先了解一下Java中的线程池的参数。
(1)线程池的参数介绍
Java 中使用 ThreadPoolExecutor
来创建自定义线程池。通过构造方法,可以传入多个参数来配置线程池,具体参数如下:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程空闲存活时间
TimeUnit unit, // 线程空闲存活时间的单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
);
参数解释:
corePoolSize(核心池大小)
- 定义:核心池大小是线程池中始终保持活动的线程数,即使线程池中的任务数较少,核心线程也会一直存在,除非设置了
allowCoreThreadTimeOut
为true
。 - 作用:决定了线程池中最小的线程数量。这些线程会一直存活,直到线程池被关闭。
- 调优建议:对于 CPU 密集型任务,可以设置为与 CPU 核心数相等;对于 IO 密集型任务,可以适当增大。
int corePoolSize = 5; // 核心线程数
maximumPoolSize(最大池大小)
- 定义:最大池大小是线程池中能够创建的最大线程数。当任务队列满了,且线程池中的线程数小于
maximumPoolSize
时,线程池会创建新线程来处理任务。 - 作用:限制线程池中最大的并发线程数。如果任务量非常大,且有大量任务需要处理,
maximumPoolSize
设得较大可以避免任务的阻塞。 - 调优建议:如果系统的硬件资源充足,且任务的数量和处理时间不确定,可以适当增加
maximumPoolSize
。
int maximumPoolSize = 10; // 最大线程数
keepAliveTime(线程存活时间)
- 定义:当线程池中的线程数超过核心线程数时,空闲线程的最大存活时间。超出这个时间,线程会被终止并从池中移除。
- 作用:如果线程池中的线程多于核心线程数,但线程在一定时间内未被使用,那么这些线程会被回收。
- 调优建议:对于任务量变化大的应用,可以适当调整
keepAliveTime
,以节省资源。对于高并发任务,可以适当增加此值。
long keepAliveTime = 60L; // 线程存活时间,单位为秒
unit(时间单位)
- 定义:
keepAliveTime
参数的时间单位,通常是TimeUnit
类提供的常量,如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
、TimeUnit.MINUTES
等。 - 作用:决定
keepAliveTime
使用的单位,方便开发者在设置时选择不同的时间粒度。 - 调优建议:如果线程池中的线程空闲时间较短,可以选择秒作为时间单位;如果线程空闲时间较长,选择分钟等较大时间单位。
TimeUnit unit = TimeUnit.SECONDS; // 设置时间单位为秒
workQueue(任务队列)
- 定义:线程池中的任务队列,用于存储等待执行的任务。常见的任务队列有
LinkedBlockingQueue
、ArrayBlockingQueue
、SynchronousQueue
等。 - 作用:当线程池中的线程数量达到
corePoolSize
时,新提交的任务会被放入任务队列等待执行。任务队列的选择直接影响线程池的性能。 - 调优建议:对于任务数量不确定的情况,可以选择无界队列(如
LinkedBlockingQueue
);如果需要限制队列的大小,则可以使用有界队列(如ArrayBlockingQueue
)。
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 设置任务队列容量为100
handler(拒绝策略)
- 定义:当线程池中的线程数达到
maximumPoolSize
,且任务队列已满时,新的任务提交就会被拒绝。此时可以通过RejectedExecutionHandler
处理任务的拒绝。 - 作用:拒绝策略决定了当任务无法被线程池处理时的处理方式。常见的拒绝策略有:
AbortPolicy
:直接抛出RejectedExecutionException
,默认策略。CallerRunsPolicy
:由调用线程处理该任务,避免任务丢失。DiscardPolicy
:直接丢弃任务。DiscardOldestPolicy
:丢弃队列中最旧的任务。
- 调优建议:如果任务丢失不可接受,推荐使用
CallerRunsPolicy
。如果可以容忍任务丢失,则可以选择DiscardPolicy
。
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 使用调用者运行策略
threadFactory(线程工厂)
- 定义:线程池使用线程工厂来创建新的线程。可以自定义线程工厂,以定制线程的创建过程(如设置线程的名称、优先级、是否为守护线程等)。
- 作用:
threadFactory
允许开发者控制线程的创建过程,特别是在需要对线程进行一些特殊配置(如设置线程名称、线程优先级、守护线程等)时非常有用。 - 调优建议:通常情况下,使用默认的线程工厂就足够了。如果需要自定义线程行为,可以实现
ThreadFactory
接口。
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("CustomThread-" + thread.getId());
return thread;
}
};
通过上述对线程池中的参数的讲解,我们就大致的了解了Java中线程池该如果创建了,那么现在让我们使用一个案例将上述所讲的串联起来,以下为一个自定义线程池:
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); // 任务队列
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("CustomThread-" + thread.getId());
return thread;
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("执行任务 " + taskId + ",线程: " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
在这个示例中,我们使用了自定义的线程池参数来创建一个 ThreadPoolExecutor
,并提交了任务来执行。通过配置这些参数,我们能够更好地控制线程池的行为,确保系统在高并发条件下高效运行。
(2)使用 Executors
创建常见的线程池
在Java中除了ThreadPoolExecutor之外,Executors
工厂类也为我们提供了几种常用的线程池创建方法,下面是几种常见线程池的创建和使用方法
【1】newFixedThreadPool(int nThreads)
- 固定线程数线程池
这种线程池创建一个固定大小的线程池,线程池中的线程数在创建后保持不变。当所有线程都处于工作状态时,新任务将进入等待队列中,直到有线程空闲出来。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定线程数量为 3 的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
});
}
// 关闭线程池
executor.shutdown();
}
}
这种方式创建线程池的特点:
- 适用于执行长期任务,性能稳定。
- 线程池中的线程数固定,不会变化。
- 如果所有线程都在工作,新的任务会被放入等待队列中,等待有空闲线程时执行。
【2】newCachedThreadPool()
- 可缓存线程池
这种线程池会根据任务需要创建新线程,并复用先前构建的线程。池中的线程如果在 60 秒内都没有被使用,则会被终止并从池中移除。
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.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
});
}
// 关闭线程池
executor.shutdown();
}
}
这种方式创建线程池的特点:
- 适用于执行大量短期任务。
- 当线程空闲 60 秒后自动回收,避免资源浪费。
- 线程池大小不固定,按需动态分配。
【3】newSingleThreadExecutor()
- 单线程线程池
这种线程池始终使用唯一的工作线程来执行任务,所有任务按提交顺序执行。如果该线程异常终止,一个新线程会取而代之,继续执行后续任务。
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.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
});
}
// 关闭线程池
executor.shutdown();
}
}
这种方式创建线程池的特点:
- 适用于需要保证任务顺序执行的场景。
- 只有一个线程工作,所有任务会按顺序执行。
- 可确保任务按提交顺序执行。
【4】newScheduledThreadPool(int corePoolSize)
- 定时/周期性线程池
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(2);
// 延迟 2 秒后执行任务
executor.schedule(() -> {
System.out.println("延迟 2 秒后执行任务");
}, 2, TimeUnit.SECONDS);
// 延迟 1 秒后开始执行任务,之后每 3 秒执行一次
executor.scheduleAtFixedRate(() -> {
System.out.println("每 3 秒执行一次任务");
}, 1, 3, TimeUnit.SECONDS);
// 关闭线程池
executor.shutdown();
}
}
这种方式创建线程池的特点:
- 适用于需要周期性执行任务的场景。
- 可以指定延迟执行,也可以按照固定的时间间隔循环执行。
以上就是使用Java中使用Executors
工厂类来创建线程池的方式了!至此我们就了解了Java中该如何创建并使用线程池了。
3.为什么要使用线程池
在了解完了如何在Java中使用线程池之后,可能读者就会发问了,我们为什么要使用线程池呢?线程池有什么优点呢?那么我们这就解释一下为什么使用线程池。
(1)降低资源的消耗
线程池通过复用线程池中的线程,避免了频繁的线程创建和销毁,从而降低了资源消耗。每次创建线程的成本较高,尤其是在并发量大的场景中,频繁地创建和销毁线程会导致系统性能下降。线程池通过维持一定数量的线程,复用这些线程处理任务,减少了频繁创建线程的开销。
示例:
public class ThreadCreationTest {
public static void main(String[] args) {
long startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
// 模拟一些简单的计算任务
for (int j = 0; j < 1000; j++) {
Math.sqrt(j);
}
}).start();
}
long endTime = System.nanoTime();
System.out.println("线程创建和销毁的时间: " + (endTime - startTime) + " 纳秒");
// 使用线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
startTime = System.nanoTime();
for (int i = 0; i < 1000; i++) {
executorService.submit(() -> {
// 模拟一些简单的计算任务
for (int j = 0; j < 1000; j++) {
Math.sqrt(j);
}
});
}
executorService.shutdown();
endTime = System.nanoTime();
System.out.println("线程池的时间: " + (endTime - startTime) + " 纳秒");
}
}
上面的代码模拟了两种方式:直接创建线程和使用线程池处理任务。通过对比这两者的时间消耗,我们就可以看到线程池显著减少了线程创建和销毁的开销。
(2)提高速度
线程池通过预先创建一定数量的线程,可以在任务到来时迅速响应,当任务提交到线程池时,如果有空闲线程,线程池可以立即开始执行任务,避免了任务排队等待线程创建的时间,确保任务尽可能快地被处理。
示例:
public class RequestHandler {
private static final int THREAD_POOL_SIZE = 10;
private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
public static void handleRequest(int requestId) {
executorService.submit(() -> {
try {
System.out.println("处理请求 " + requestId + " 的线程:" + Thread.currentThread().getName());
Thread.sleep(200); // 模拟处理请求的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
handleRequest(i);
}
executorService.shutdown();
}
}
在这个例子中,我们模拟了 50 个请求的并发处理。线程池使用了 10 个线程来并发处理请求,并且任务的执行时间(Thread.sleep(200)
)模拟了请求的处理过程。通过线程池,多个请求可以同时被多个线程处理,而不需要等待线程的创建。
(3)提高线程的可管理性
线程池不仅能有效地复用线程,还提供了线程的生命周期管理。通过合理设置线程池的参数,开发者可以控制线程的创建、销毁以及空闲时的回收方式,从而确保系统在负载较重时仍能稳定运行。
示例:
import java.util.concurrent.*;
public class DynamicThreadPoolTest {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 20;
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY)
);
public static void handleTask(int taskId) {
executor.submit(() -> {
try {
System.out.println("任务 " + taskId + " 正在执行,线程: " + Thread.currentThread().getName());
Thread.sleep(200); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
handleTask(i);
}
// 动态调整线程池的大小
executor.setCorePoolSize(7);
executor.setMaximumPoolSize(15);
System.out.println("线程池已调整为新的配置");
// 关闭线程池
executor.shutdown();
}
}
在上述代码中,线程池最初使用了 5 个核心线程和 10 个最大线程,但在任务提交过程中,我们根据负载情况动态调整了线程池的大小(通过调用 setCorePoolSize
和 setMaximumPoolSize
)。这种动态调整可以有效应对负载变化,从而优化线程池的性能。
这样我们就理解了,为什么要使用线程池了!!!