一、为什么需要线程池
1、线程的创建
Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:
- 必须为线程堆栈分配和初始化大量内存块,其中包括至少1MB的栈内存
- 需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程
2、线程销毁
Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范锁允许的。在Java中,线程的销毁(即线程结束其执行并释放资源)本身并不是一个效率特别低下的操作,但它可能涉及一些开销,尤其是在大量线程频繁创建和销毁的场景中。以下是一些影响线程销毁效率的因素:
- 资源释放:当线程结束时,它需要释放其占用的系统资源,如栈内存、线程ID、线程状态等。这些资源的释放操作需要一定的时间。
- 并发安全性:如果在多线程环境下进行线程销毁,还需要考虑线程的并发安全性,避免出现并发访问共享资源的问题,这可能需要引入同步机制,增加了复杂性和性能开销。
- 垃圾回收:线程对象和其他与之关联的对象在不再被引用时,会被Java的垃圾回收器标记为可回收。垃圾回收过程本身可能会产生一些开销,尤其是在有大量对象需要回收时。
- 上下文切换:如果线程的销毁导致操作系统的上下文切换(例如,从用户态切换到内核态进行资源清理),那么这也可能产生一定的开销。
二、优点
1、降低资源消耗
通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
2、提高响速度
当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、线程管理
进行统一分配、调优和监控。
三、架构说明
Java中线程池是通过Executor框架实现,该框架中用到了Executor、Executors(代表工具类)、ExecutorService、ThreadPoolExecutor这几个类
四、Executors工具类
1、介绍
Executors 是 Java 并发包 java.util.concurrent 中的一个工具类,它提供了一组用于创建线程池的方法。线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的 ThreadFactory 创建一个新线程。
Executors 工具类的主要目的是简化线程池的创建和管理。通过它,你可以很容易地创建固定大小的线程池、可缓存的线程池、定时线程池等,而无需手动处理 ThreadPoolExecutor 的复杂配置。
2、Executors工具类常用的方法
(1)newFixedThreadPool(int nThreads):
创建一个固定大小的线程池。如果所有线程都在工作,那么新提交的任务会在一个队列中等待,直到有线程可用。
new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
(2) newCachedThreadPool()
创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是空闲线程会得到及时回收。
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>())
(3)newSingleThreadExecutor()
创建一个单线程的线程池。这意味着在任何时候,都只有一个任务在执行。
new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
(4)newScheduledThreadPool(int corePoolSize):
创建一个定时线程池,它可以在给定的延迟后运行命令,或者定期地执行命令。
五、ThreadPoolExecutor
1、构造函数
ThreadPoolExecutor(int corePoolSize, //核心线程数,即使线程空闲也不回收
int maximumPoolSize, //线程数的上限
long keepAliveTime, //线程最大空闲时长
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //任务的排队队列
ThreadFactory threadFactory, //新线程的产生方式
RejectedExecutionHandler handler) //拒绝策略
2、参数含义
-
corePoolSize:线程池中核心线程的数量
-
keepAliveTime:线程空闲的时间
-
unit:keepAliveTime的单位
-
workQueue:用来保存等待执行的任务的阻塞队列,常见的阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
- LInkedBlockingQueue:是 一个基于链表接口的阻塞队列,此队列按FIFO排序元素
- SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列
-
threadFactory:用于设置创建线程的工厂,默认是DefaultThreadFactory
-
handler:RejectedExecutionHandler, 线程池的拒绝策略,分类:
- AbortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
- 自定义策略:根据应用场景需要实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务
3、线程池的处理流程
4、线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用情况快速定位问题。线程池的监控是确保线程池稳定性和性能的关键环节。以下是一些建议的监控方法和工具:
- 使用监控工具:可以使用一些专门的监控工具,如VisualVM、Grafana、Prometheus等,这些工具能够实时监控线程池的运行情况,并绘制相应的图表和指标。通过这些工具,可以获取线程池的状态信息,如线程数量、活跃线程数、任务队列长度等,从而及时发现并解决潜在问题。
- 继承线程池并重写方法:通过继承线程池并重写其部分方法,可以在任务执行前、执行后和线程池关闭前进行自定义操作。例如,可以重写beforeExecute、afterExecute和terminated方法,以监控任务的平均执行时间、最大执行时间和最小执行时间等关键指标。
- 使用线程池的参数进行监控:线程池中有一些关键参数,通过监控这些参数,可以了解线程池的工作负载、任务完成情况和资源利用情况。线程池的监控参数主要包括以下几个方面:
- 任务执行情况指标:
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池已完成的任务数量。
- getActiveCount():线程池中正在执行任务的线程数量。
- getCompletedTaskCount():线程池已完成的任务数量。
- 线程使用情况指标:
- poolSize或 getPoolSize():线程池当前的线程数量。
- getCorePoolSize():线程池的核心线程数量。
- getLargestPoolSize():线程池曾经创建过的最大线程数量。
- queueSize:还剩多少个任务未执行。
- 异常情况指标:
- taskRejectedCount:任务拒绝次数。
- taskTimeoutCount:任务超时次数。
- 任务执行情况指标:
- 自定义监控指标:除了利用线程池自带的参数和工具,还可以根据具体需求自定义监控指标。例如,可以监控线程池的队列饱和度、单位时间内提交任务的速度与消费速度的比值等,以便更全面地了解线程池的性能状况。
综上所述,线程池的监控是一个综合性的任务,需要结合多种方法和工具来实现。通过有效的监控,可以及时发现并解决线程池中的问题,从而确保系统的稳定性和性能。
5、线程池优雅关闭:(一般情况下,线程池启动后建议手动关闭)
(1)线程池的5中状态:
- RUNNING:线程池创建之后的初始化状态,这种状态下可以执行任务
- SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕
- STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程
- TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法
- TERMINATED:执行完terminated()钩子方法之后的状态
(2)线程池状态转换规则
- 线程池创建之后状态为RUNNING。
- 执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN
- 执行线程池shutdownNow()实例方法,会使线程池状态从RUNNING转变为STOP
- 当线程池处于SHUTDOWN状态时,执行其shutdownNow()方法会将其状态转变为STOP
- 等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING
- 执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED
(3)关闭线程池方法
- shutdown:线程池的状态设置成SHUTDOWN状态,拒绝新任务的提交,并等待所有任务有序地执行完毕,中断没有正在执行任务的线程
- shutdownNow:将线程池的状态设置为STOP,然后尝试停止所有的正在执行或者暂停任务的线程
源码解析
1、java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// ctl是一个Integer的原子对象,主要作用:(1)记录线程池的状态信息;(2)记录线程池工作线程的数量
int c = ctl.get();
// workerCountOf(c):工作线程的数量
// 工作线程的数量如果小于核心线程的数量,添加一个worker,参数1:要执行的任务, 参数2:true代表添加核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// isRunning(c):线程池是否处于运行状态;尝试向任务队列workQueue中添加一个任务
if (isRunning(c) && workQueue.offer(command)) {
// 任务添加成功,,在检查一次线程池状态
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}