Java线程池相关
# Java基础篇 | 线程池相关
思维导图
# 1. 前言
大家好,我是scholar (opens new window),[继上一篇我们没有学习完的多线程]
,这一节我们主要学习多线程中的线程池相关内容。好了,话不多说让我们开始吧😎😎😎。
# 2. 线程状态介绍
在学习线程池之前,我们有必要先了解一下关于线程的集中状态。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中的线程
状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:
通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下:
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread() 只有线程象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的start 方法,那么此时线程对象进入了RUNNABLE 状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE )也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked 状态;当该线程持有锁时,该线程将变成Runnable 状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait() 、join() 方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long) ,Object.wait(long) 、join(long) 。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
那么线程这几种状态是如何进行转换的呢,我们来看一下下面这张图
# 3. 线程池
# 3.1 线程池-概述
线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作。
线程池作用:
- 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死。
- 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的核心思想: 把线程创建好了之后,放到池子里,需要使用线程,就直接从池子里取,而不是通过系统来创建。当线程用完了,又放回池子里,而不是通过系统来销毁!!(线程复用,同一个线程可以被重复使用,来处理多个任务。)
为什么从线程池取线程要比从系统申请效率更高呢
因为从线程池取线程是纯粹的用户态操作,而从系统创建线程会涉及到内核态和用户态的切换。
用户态切换到内核态需要进行“上下文切换”。这个过程包括保存当前进程的状态、加载内核进程的状态等一系列操作,消耗相对较大的时间和资源。而在用户态执行的操作不需要这种切换,因此成本更低。
# 3.2 线程池解决了什么问题
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
# 3.3 ExecutorService接口
ExecutorService
是一个接口,它定义了一组方法,这些方法提供了线程池的操作和管理功能,如任务提交、线程池关闭、任务执行情况查询等。任何实现了ExecutorService
接口的类都必须提供这些方法的具体实现。
- 通过
Executors
创建的线程池:如Executors.newFixedThreadPool()
或Executors.newCachedThreadPool()
等方法,返回的都是ExecutorService
接口的实现。 - 直接使用
ThreadPoolExecutor
创建的线程池:ThreadPoolExecutor
本身就是ExecutorService
接口的一个具体实现,提供了更灵活的配置选项。
# 3.3.1 提交方法
ExecutorService 类 API:
方法 | 说明 |
---|---|
void execute(Runnable command) | 执行任务(Executor 类 API) |
Future<?> submit(Runnable task) | 提交任务 task() |
Future submit(Callable task) | 提交任务 task,用返回值 Future 获得任务执行结果 |
List<Future> invokeAll(Collection<? extends Callable> tasks) | 提交 tasks 中所有任务 |
List<Future> invokeAll(Collection<? extends Callable> tasks, long timeout, TimeUnit unit) | 提交 tasks 中所有任务,超时时间针对所有task,超时会取消没有执行完的任务,并抛出超时异常 |
T invokeAny(Collection<? extends Callable> tasks) | 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 |
execute 和 submit 都属于线程池的方法,对比:
- execute 只能执行 Runnable 类型的任务,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成 FutureTask,然后调用 execute 执行。
- execute():如果任务中有未捕获的异常,它会在执行该任务的线程中抛出,可能导致线程终止,但不会影响调用
execute()
的线程。 - submit():如果任务中有异常,它会被封装在
Future
对象中,可以通过调用Future.get()
方法在调用submit()
的线程中捕获并处理。
# 3.3.2 关闭方法
ExecutorService 类 API:
方法 | 说明 |
---|---|
void shutdown() | 线程池状态变为 SHUTDOWN,等待任务执行完后关闭线程池,不会接收新任务,但已提交任务会执行完,而且也可以添加线程(不绑定任务) |
List shutdownNow() | 线程池状态变为 STOP,用 interrupt 中断正在执行的任务,直接关闭线程池,不会接收新任务,会将队列中的任务返回 |
boolean isShutdown() | 不在 RUNNING 状态的线程池,此执行者已被关闭,方法返回 true |
boolean isTerminated() | 线程池状态是否是 TERMINATED,如果所有任务在关闭后完成,返回 true |
boolean awaitTermination(long timeout, TimeUnit unit) | 调用 shutdown 后,由于调用线程不会等待所有任务运行结束,如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待 |
# 3.4 JDK中自带的线程池-Executors
Executors类是Java标准库(Java Standard Edition,JDK)中提供的一个工具类,它属于java.util.concurrent
包。Executors
类提供了一系列静态方法来创建不同类型的线程池,这些线程池底层实际上是基于ThreadPoolExecutor
和ScheduledThreadPoolExecutor
这两个类的实现。
Executors
类中提供的静态工厂方法主要包括:
- newFixedThreadPool(int nThreads): 创建一个固定数量的线程池。这种类型的线程池在应用程序的生命周期内拥有固定数量的线程,适用于负载相对稳定的场景。
- newCachedThreadPool(): 创建一个可缓存的线程池,可以灵活回收空闲线程,若无可回收则新建线程。适合处理大量短暂异步任务。
- newSingleThreadExecutor(): 创建一个单线程的Executor。这保证了所有提交的任务都按顺序执行,并且在任一时间点,不会有多个线程活动。
- newScheduledThreadPool(int corePoolSize): 创建一个可以延迟执行或周期性执行任务的线程池。
小试牛刀:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建一个固定数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 提交第一个任务给线程池执行
executorService.submit(() -> {
// 打印当前线程的名称,显示它在执行任务
System.out.println(Thread.currentThread().getName() + " 在执行任务1");
});
// 提交第二个任务给线程池执行
executorService.submit(() -> {
// 同样打印当前线程的名称,显示它在执行任务
System.out.println(Thread.currentThread().getName() + " 在执行任务2");
});
// 关闭线程池,不接受新任务,已提交的任务继续执行
executorService.shutdown();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3.5 基于ThreadPoolExecutor构建线程池
ThreadPoolExecutor
是Java并发框架中提供的一个灵活且强大的线程池实现,允许开发者详细配置线程池的各个方面,包括核心线程数、最大线程数、空闲线程存活时间、任务队列等。它直接实现了ExecutorService
接口,并提供了更为丰富的构造器参数,使得开发者可以根据自己的具体需求来创建和管理线程池。
ThreadPoolExecutor中定义了七大核心属性,这些属性是线程池实现的基石。
线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:
可以看到,其需要如下几个参数:
- corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
核心线程数与最大线程数的关系和区别?
- 容量上限:核心线程数定义了线程池的基本大小,最大线程数定义了线程池的最大边界。
- 线程回收:默认情况下,核心线程即使处于空闲状态也不会被回收(除非
allowCoreThreadTimeout
设置为true
),而非核心线程(即超过核心线程数的部分)在空闲一定时间(keepAliveTime
)后会被回收。 - 任务增长响应:当任务数增加,超过核心线程能处理的范围时,线程池可以暂时增加线程到最大线程数来处理增加的任务。一旦任务量减少,超过核心线程数的线程在空闲一定时间后会被回收。
线程池使用流程如下:
public class ThreadPoolExample {
public static void main(String[] args) {
// 定义核心线程数
int CORE_POOL_SIZE = 5;
// 定义最大线程数
int MAXIMUM_POOL_SIZE = 10;
// 定义空闲线程的存活时间
long KEEP_ALIVE = 1L;
// 创建任务队列,用于存放等待执行的任务
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(50);
// 创建线程工厂,用于创建新线程
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "my-pool-" + threadNumber.getAndIncrement());
return t;
}
};
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
CORE_POOL_SIZE, // 核心线程数
MAXIMUM_POOL_SIZE, // 最大线程数
KEEP_ALIVE, // 空闲线程等待新任务的最长时间
TimeUnit.SECONDS, // KEEP_ALIVE的时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂
new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
);
// 向线程池提交任务
for (int i = 0; i < 20; i++) {
int finalI = i;
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + finalI);
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
// 逐步关闭线程池,不再接收新任务,但会等待队列里现有任务执行完毕
// 注意:在实际使用场景中,通常只需要调用shutdown()或shutdownNow()中的一个。
threadPool.shutdown();
// 或者,如果需要立即关闭线程池,可以调用shutdownNow()
// List<Runnable> notExecutedTasks = threadPool.shutdownNow();
}
}
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 3.6 线程池的工作原理
下面来描述一下线程池工作的原理,提交一个任务到线程池中,线程池的处理流程如下:
- 核心线程检查:首先检查当前运行的线程是否少于核心线程数(
corePoolSize
)。如果是,即使其他工作线程是空闲的,线程池也会创建新的线程来执行新提交的任务。 - 任务队列检查:如果当前运行的线程数等于或超过核心线程数,线程池会尝试将提交的任务放入队列。这个队列通常是
BlockingQueue
类型,用于存放等待执行的任务。- 如果任务能成功入队,那么任务暂时会等待在队列中直到有工作线程可用来执行它。
- 如果任务无法入队(比如队列已满),线程池将进入下一个处理阶段。
- 最大线程数检查:如果任务无法入队,线程池会继续检查当前运行的线程数是否少于最大线程数(
maximumPoolSize
)。- 如果当前运行的线程数少于最大线程数,线程池会尝试创建新的线程来执行任务。
- 如果当前运行的线程数已达到最大线程数,线程池将无法处理更多任务,此时会采用拒绝策略(
RejectedExecutionHandler
)。
- 拒绝策略:当线程池无法接受新任务时(即任务队列已满且已达到最大线程数),将会根据配置的拒绝策略来处理新提交的任务。默认的拒绝策略是抛出
RejectedExecutionException
异常,但还有其他几种策略可用,比如调用者运行、静默丢弃等。
总结
线程池的工作原理是先尽量使用核心线程,然后使用任务队列缓存任务,最后在必要时扩展到最大线程数,如果还处理不了就根据拒绝策略处理新提交的任务。这个设计旨在为执行多个任务提供灵活的线程管理方式,同时最大限度地减少资源消耗。
# 4. 线程池的参数
# 4.1 任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
- DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
- SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
- LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
- LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。
注意事项
注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。
# 4.2 线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:
/**
* The default thread factory.
*/
private static class DefaultThreadFactory implements ThreadFactory {
// 使用原子整数记录工厂创建的线程池数量,确保每个线程池的编号是唯一的
private static final AtomicInteger poolNumber = new AtomicInteger(1);
// 线程组,所有通过这个工厂创建的线程都属于这个组
private final ThreadGroup group;
// 记录该工厂创建的线程数量,确保每个线程的编号是唯一的
private final AtomicInteger threadNumber = new AtomicInteger(1);
// 线程名称前缀,包含了线程池编号,用于区分不同线程池中的线程
private final String namePrefix;
// 构造方法
DefaultThreadFactory() {
// 获取系统的安全管理器
SecurityManager s = System.getSecurityManager();
// 如果存在安全管理器,则使用该管理器的线程组;否则,使用当前线程的线程组
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
// 设置线程名称前缀,格式为"pool-N-thread-",其中N是线程池编号
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
// 创建新线程的方法
public Thread newThread(Runnable r) {
// 创建新线程,设置线程的线程组、运行任务、线程名称和堆栈大小(这里设置为0,表示忽略这个参数)
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
// 如果新线程被创建为守护线程,则设置为非守护线程
if (t.isDaemon())
t.setDaemon(false);
// 如果新线程的优先级不是正常优先级,则设置为正常优先级
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
// 返回创建的线程
return t;
}
}
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
33
34
35
36
37
38
39
40
41
42
# 4.3 拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 ThreadPoolExecutor
已经为我们实现了 4 种拒绝策略:
使用语法:new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
- AbortPolicy(默认):会直接丢弃任务并抛出 拒绝执行异常(RejectedExecutionException) 。
- CallerRunsPolicy:把任务交给提交任务的(main)线程来执行。
- DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
# 5. 详解Executors提供的线程池
下面将详细介绍Executors已经为我们封装好了的 4 种常见的功能线程池,如下:
- 定长线程池(FixedThreadPool): 通过
Executors.newFixedThreadPool(int nThreads)
方法创建。这种类型的线程池具有固定的线程数量。一旦创建,线程池的大小不变。如果所有线程都在工作,额外的任务会在队列中等待。这种类型的线程池适合于负载比较重的服务器。 - 定时线程池(ScheduledThreadPool): 通过
Executors.newScheduledThreadPool(int corePoolSize)
方法创建。这种线程池支持定时及周期性任务执行。比如,可以调度一个任务在某段时间后执行,或者周期性地重复执行。 - 可缓存线程池(CachedThreadPool): 通过
Executors.newCachedThreadPool()
方法创建。这种线程池没有固定大小,可以根据需求自动的增加新的线程,空闲线程会被回收。这种类型的线程池适合执行大量的短期异步任务。 - 单线程化线程池(SingleThreadExecutor): 通过
Executors.newSingleThreadExecutor()
方法创建。这种线程池中只有一个线程,它确保所有的任务都在同一个线程中按顺序执行。适用于需要顺序执行任务的场景,并且也能保证在任意时间点不会有多个线程是活动的。
# 5.1 定长线程池(FixedThreadPool)
创建方法的源码:
// 创建一个固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
// nThreads: 指定了线程池中的线程数量
// 0L: 非核心线程的闲置超时时间,这里设置为0表示非核心线程不会因闲置被回收(实际上在FixedThreadPool中不存在非核心线程)
// TimeUnit.MILLISECONDS: 超时时间的单位,这里用毫秒
// new LinkedBlockingQueue<Runnable>(): 任务队列,使用链表结构的无界队列来存储待执行的任务
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 这个重载版本允许用户指定一个ThreadFactory来创建新线程
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
// threadFactory: 用户可以自定义线程创建的方式,例如设置线程名字,设置为守护线程等
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:控制线程最大并发数。
使用示例:
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
2
3
4
5
6
7
8
9
10
# 5.2 定时线程池(ScheduledThreadPool )
创建方法的源码:
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
// Executors类中提供的创建定时线程池的静态方法
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
// 创建一个具有指定核心线程数的ScheduledThreadPoolExecutor实例
return new ScheduledThreadPoolExecutor(corePoolSize);
}
// ScheduledThreadPoolExecutor的构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
// 调用ThreadPoolExecutor的构造方法,设置线程池参数
super(corePoolSize, // 核心线程数,也是线程池的最小线程数
Integer.MAX_VALUE, // 最大线程数设置为Integer的最大值,表示线程数可以非常大
DEFAULT_KEEPALIVE_MILLIS, // 非核心线程的空闲存活时间,默认为10毫秒
TimeUnit.MILLISECONDS, // 空闲存活时间的单位,这里为毫秒
new DelayedWorkQueue()); // 使用DelayedWorkQueue,这是一个无界的延迟队列,用于存放待执行的定时任务
}
// 提供一个重载的构造方法,允许指定一个ThreadFactory
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
// 使用自定义的线程工厂来创建线程
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
// ScheduledThreadPoolExecutor的构造方法,接受一个ThreadFactory参数
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
// 调用ThreadPoolExecutor的构造方法,设置线程池参数,包括自定义的线程工厂
super(corePoolSize, // 核心线程数
Integer.MAX_VALUE, // 最大线程数为Integer的最大值
DEFAULT_KEEPALIVE_MILLIS, // 非核心线程的空闲存活时间
TimeUnit.MILLISECONDS, // 存活时间单位
new DelayedWorkQueue(), // 使用DelayedWorkQueue作为工作队列
threadFactory); // 使用自定义的线程工厂
}
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
33
34
35
36
- 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
- 应用场景:执行定时或周期性的任务。
使用示例:
// 1. 创建 定时线程池对象并设置线程池核心线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
2
3
4
5
6
7
8
9
10
11
# 5.3 可缓存线程池(CachedThreadPool)
创建方法的源码:
// 无参数版本的newCachedThreadPool方法
public static ExecutorService newCachedThreadPool() {
// 创建并返回一个ThreadPoolExecutor实例,具有以下特性:
return new ThreadPoolExecutor(
0, // 核心线程数设置为0,意味着线程池不保留任何空闲线程
Integer.MAX_VALUE, // 允许创建线程数量的上限设置为Integer的最大值,理论上允许无限创建新线程
60L, // 空闲线程的存活时间设置为60秒
TimeUnit.SECONDS, // 存活时间单位为秒
new SynchronousQueue<Runnable>()); // 使用SynchronousQueue作为工作队列,这种队列不存储元素,每个插入操作必须等待另一个线程的移除操作
}
// 带有ThreadFactory参数的newCachedThreadPool方法
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
// 该构造方法允许用户提供一个ThreadFactory来自定义创建线程的方式
return new ThreadPoolExecutor(
0, // 核心线程数为0
Integer.MAX_VALUE, // 最大线程数为Integer的最大值
60L, // 空闲线程在终止前的最长空闲时间为60秒
TimeUnit.SECONDS, // 时间单位为秒
new SynchronousQueue<Runnable>(), // 使用SynchronousQueue作为工作队列
threadFactory); // 使用用户提供的ThreadFactory来创建新线程
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
- 应用场景:执行大量、耗时少的任务。
使用示例:
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
2
3
4
5
6
7
8
9
10
# 5.4 单线程化线程池(SingleThreadExecutor)
创建方法的源码:
// 无参数版本的newSingleThreadExecutor方法
public static ExecutorService newSingleThreadExecutor() {
// 返回一个ExecutorService对象,这里使用了FinalizableDelegatedExecutorService包装了ThreadPoolExecutor
// 以提供单线程执行环境,并确保线程池的最终化
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(
1, // 核心线程数设置为1,确保同时只有一个任务在执行
1, // 最大线程数也设置为1,保持线程池中始终只有一个线程
0L, // 空闲线程的存活时间设置为0,因为线程数固定,此设置实际上无效
TimeUnit.MILLISECONDS, // 设置时间单位,这里的设置实际上不影响操作,因为存活时间是0
new LinkedBlockingQueue<Runnable>() // 使用无界的LinkedBlockingQueue作为工作队列
));
}
// 带有ThreadFactory参数的newSingleThreadExecutor方法
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
// 与上面相似,但允许用户通过ThreadFactory参数自定义线程的创建
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(
1, // 核心线程数为1
1, // 最大线程数为1
0L, // 空闲线程存活时间为0
TimeUnit.MILLISECONDS, // 时间单位为毫秒
new LinkedBlockingQueue<Runnable>(), // 使用LinkedBlockingQueue作为队列
threadFactory // 使用自定义的线程工厂
));
}
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
- 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
使用示例:
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
2
3
4
5
6
7
8
9
10
# 5.5 对比
# 6. 总结
Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
其实 Executors 的 4 个功能线程有如下弊端:
- FixedThreadPool 和 SingleThreadExecutor:这两种线程池默认使用无界的
LinkedBlockingQueue
作为工作队列。在高负载情况下,未处理的任务可能会不断积累,最终导致内存消耗过大,甚至出现OutOfMemoryError
异常。 - CachedThreadPool 和 ScheduledThreadPool:这两种线程池允许创建的线程数量最大可达
Integer.MAX_VALUE
,这在理论上意味着线程数量可以无限增长。在实际应用中,过多的线程会消耗大量的系统资源,不仅会降低系统性能,还可能导致系统崩溃。
阿里巴巴的Java开发规范指出,直接使用Executors
类提供的快捷方法创建线程池虽然方便,但可能会导致资源耗尽的风险,因此不推荐在生产环境中使用。
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是scholar,一个在互联网行业的小白,立志成为更好的自己。
如果你想了解更多关于scholar (opens new window),可以关注公众号-书生带你学编程,后面文章会首先同步至公众号。