从Java线程池到池化思想
Java线程创建和销毁的开销 中提到了Java线程创建和销毁的开销,因此我们可以使用“池”化的思想,每一次有新的任务需要处理时,直接从线程池中拿出来一个空闲的线程去执行,这样减少了创建线程的开销,同时在任务处理完成后将线程归还给线程池,给下一个任务使用,也减少了销毁的开销。
池化思想在我们开发中非常常见,比如:数据库连接池,Tomcat线程池,业务开发中的线程池,Netty的EventLoopGroup,甚至于HTTP的长连接也是复用TCP连接,和池化思想很相似。
为什么要使用线程池?
- 降低资源消耗,提高响应速度
- 如果任务多了,我们其实有一些衍生的管理、编排的需求,例如:【1】
- 顺序管理。任务按照什么顺序执行?(FIFO、LIFO、优先级)?
- 资源管理。同时有多少任务能并发执行?允许有多少任务等待?
- 错误处理。如果负载过多,需要取消任务,应该选哪个?如何通知该任务?
- 生命周期管理。如何在线程开始或结束时做一些操作?
线程池的参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 保持存活时间,当线程池的线程数多于corePoolSize时,当空闲线程的时间超过keepALiveTime时,多于corePoolSize数量的线程会被回收(如果设置某个参数的话,核心线程数也会被回收)。这个变量存在的意义,还是为了减少资源的消耗。
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务存储队列
ThreadFactory threadFactory, // 当线程池需要新的线程的时候,会使用threadFactory创建
RejectedExecutionHandler handler // 任务拒绝策略
) {
// ...
}
当新的任务来时的流程:
- 看当前的运行的线程数是不是小于corePoolSize,如果是的话就创建新的线程
- 如果目前运行的线程数等于corePoolSize的话,就看任务存储队列是不是空的,如果是的话,就把任务往这里面放
- 如果任务队列也满了,那就再创建新的线程,直至等于maximumPoolSize,这里队列可以是无界的,maxmumPoolSize也可以是很大的值
- 最后根据拒绝策略,去拒绝新来的任务
常见的BlockingQueue的实现类有:
- ArrayBlockingQueue,数组,ReentrantLock,有界
- LinkedBlockingQueue,链表,ReentrantLock,有界
- LinkedTransferQueue,链表,CAS,无界。需要注意的是,队列是无界的话,线程池等待队列有可能会变得越来越大,可能会导致频繁的GC,甚至于OOM
拒绝策略:
- 丢弃任务,不重要的任务,比如打印日志
- 调用方执行
- 抛异常
Executors中几种线程池
-
// 可能占用的内存会很大 newFixedThreadPool(n, n, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), default, default);
-
newSingleThreadExecutor // 同样的问题 new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), default, default);
-
newCachedThreadPool // 线程数可能特别多,队列长度为0 new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), default, default);
-
newScheduledThreadPool // 同样的线程数可能特别多 corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), default, default
所以最好根据实际的业务场景自定义一个线程池
停止线程池
- shutdown():现有的任务还是会执行,但不再接受新的任务
- isShutdown():来判断是不是执行过了shutdown()
- isTerminated():用于检查线程池是否已经终止。如果线程池的所有任务都已经执行完成并且线程池已经调用了 shutdown 方法,则返回 true;否则返回 false
- awaitTermination(long timeout, TimeUnit unit):用于等待线程池的所有任务执行完成,并设置等待指定的超时时间
- shutdownNow():用于立即关闭线程池。调用该方法会尝试停止所有正在执行的任务,并返回一个未执行的任务列表。该方法会使用 Thread.interrupt() 方法尝试中断正在执行的任务,但并不能保证任务一定会停止
线程池原理
实际问题
-
快速相应请求,如:请求A依赖于B、C、D,且B、C、D互相没有依赖关系,则可以将B、C、D放入到一个线程池中执行,这样相应的实现取决于最慢的任务。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务【2】。这个地方我认为可以这么理解:直接创建新的线程去执行是比放到任务队列中响应时间快的,但是前提是CPU的处理能力要很强,假设CPU只有一核,那么创建很多的线程提升的性能可能很有限甚至于没有提升,因为有部分时间在进行上下文切换。
-
快速处理批量任务,如统计报表,与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题【2】。吞吐量优先就需要
响应时间 vs 吞吐量,是两个矛盾的点,在垃圾回收算法和CPU的任务调度中也有体现,待补充...
数据库连接池
SpringBoot2默认连接池HikariCP【3】
Tomcat线程池
该部分参考【4】,算是笔记
Tomcat整体架构
Server // 一个JVM只有一个Server,
Service // 服务的抽象
Service
.
.
Service
Connetor // 处理连接请求
HTTP、AJP
阻塞IO,非阻塞IO
Engine // 全局Servlet引擎,每个Service只能有一个
Host // 代表虚拟主机,可以存放若干Web应用的抽象
.
.
Host
Context // Web应用的抽象,我们开发的Web应用部署到Tomcat后就会转化为Context
Wrapper // 一个Servlet就对应着一个Wrapper
Executor // 执行Service的任务
===================================
仅作为校招时的《个人笔记》,详细内容请看【参考】部分
===================================
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端