[ Java ]中的线程池工作原理,为什么不是先创建线程而是先往阻塞队列里塞任务?

149 天前
 lsk569937453

假设核心线程数为 n,最大线程数为 m 。线程池创建后,就提交了 n 个任务且这 n 个任务一直在执行,没有结束。此时再提交一个任务就会塞到任务队列里。我的疑问是新提交的这个任务为什么不是创建一个新的线程执行?。线程池不是应该首先要保证任务完成吗?

现在的逻辑是"先判断任务队列是否满再判断是否达到最大线程数",这样设计有什么优点呢?

6870 次点击
所在节点    Java
86 条回复
trzzzz
149 天前
@lsk569937453 那是你逻辑设计有问题。哥们
wqhui
149 天前
保证任务完成跟用最快速度完成任务不是一件事,保证任务完成是任务不丢,用最快速度完成任务是尽最大可能抢占资源,哪怕把别的任务干停也不无所谓。
新提交任务直接优先创建新线程执行,目的是为了尽可能快的处理掉提交的任务,但代价是可能挤压其他线程的运行,因为应用中一般都不止一个线程池,所有线程池都优先创建线程,即线程池线程数很容易就会大幅超过实际并行数,回到了用线程池前的情况。
比如总共支持 10 线程并行,有 5 个线程池,每个池 max thread 是 5,core thread 是 1 ,每个池子打满有 25 线程,优先新线程执行策略,只需要 10 个任务就吃满并行了;优先入队策略,首先得把队列打满,可能是 1w 个任务,才会出现应用有 10 线程在跑,相对并行数来讲内存队列存储任务明显不值钱且易扩容

先判断是否达到最大线程数在判断任务队列是否满了,这种需要在处理 IO 任务或者应用总线程池数非常少的时候才行
ZeawinL
149 天前
如果有这类需求,可以使用 SynchronousQueue 虚拟队列
yidinghe
149 天前
我看了一下基本上都没回答到点子上。

这个问题的起因在于 Worker 线程的工作模式:循环从队列中取任务,取不到任务就终止。取任务这里会有 keepAliveTime 的超时设定,也就是说,在没有任务的情况下,线程依然会保持 keepAliveTime 这么长时间。

这就是关键!请问,当线程数达到 corePoolSize 后,再提交新的任务时,是不是一定要马上创建新的线程???

答案是否定的!

为什么?因为此时 corePoolSize 个线程并非都是忙的,可能存在空闲的线程。这个时候我们要做的,是把这个可能存在的空闲线程利用起来,而如果不这么做而是创建新的线程,就是浪费系统资源。

怎么利用起来呢?答案就是将任务丢进队列。此时正在取任务的核心线程就会马上取到这个任务了。
yty2012g
149 天前
1 、ThreadPoolExecutor 的行为是,如果线程数<核心线程数,直接创建,否则丢队列,然后如果小于最大线程数就创建线程。
2 、基于第一条,所以 JDK 的线程池设计原则中是包含了 Core (重要的)线程和其他线程之分的,因为线程的创建是重操作。
3 、基于 OP 的表述,似乎是任务是平等的,没有重要的任务和不重要的任务之分,进而也没有重要的核心线程和一般的其他线程之分,所以认为应该先创建线程而非塞队列,那么这个和 JDK 的设计原则是不太一样的
4 、Tomcat 线程池的设计原则就是任务是平等的,因此 Tomcat 线程池的策略是先创建线程到最大线程,而后才会塞入队列
fcten
149 天前
1. 使用线程池的目的是为了减少线程的创建和销毁,而不是为了保证任务完成。
任务是否能完成受很多因素影响,瓶颈不一定在线程数量上。试图去保证任务一定完成是不切实际的,盲目创建新线程可能会让系统加速崩溃。

2. "先判断是否达到最大线程数在判断任务队列是否满了"为什么不可以?
如果使用这样的策略的话,提前创建好最大的线程数量(核心线程数=最大线程数)会比让线程池去动态创建和销毁线程更高效。动态销毁线程的唯一优势大概是节省一点点内存。

3. "先判断任务队列是否满再判断是否达到最大线程数",这样设计有什么优点呢?
减少了线程的创建和销毁。当请求量只是在短时间内超出处理能力的情况下,可以避免创建新线程。
imaple
149 天前
根本原因是无限的开线程不仅不会加快系统, 反而会增加线程切换的开销;
zhangjiashu2023
149 天前
@cheng6563 额不对把,是先创建到核心线程数量然后加入队列,队列满了再创建线程到最大线程数量
zerolinck
149 天前
不是不可以这么做,而是当前的设计更简洁一些,创建线程相关的工作再从任务队列中取任务时已经有了,在添加任务的阶段增加这些步骤的话,增加了系统复杂度。
<img src="https://imgse.com/i/pkiWV8P">
zerolinck
149 天前
pegasusz
149 天前
@BiChengfei 精辟
nothingistrue
149 天前
"先判断是否达到最大线程数在判断任务队列是否满了"为什么不可以?

为什么要可以?你定义的 n < m ,那不就是打算让它再非繁忙情况下只运行 n 个线程吗?你现在又让它在刚 n+1 个任务就开新线程,这在现实中就是瞎指挥的「领导」。
nothingistrue
149 天前
看了楼主后面的回复,我上面的回复应该是白瞎了。

追本溯源,Java 的线程池,其设计目的不是为了线程池,它是异步执行器的无奈实现。Java 内置的线程池,你只能把它当作异步执行器来看。而异步执行器,它只保证新任务能被快速接受——以不阻塞任务的提交者,不保证新任务被立刻执行——这很可能导致后续任务提交被阻塞。如果你要的是类似连接池那样的同步调用场景的池子,你得自己设计非异步执行器的线程池。

另外,即时是常规线程池,你也没办法保证任务被立即执行,因为线程启动不等于线程里面的任务开始执行。
hapeman
149 天前
线程池的本质是为了减少创建和销毁线程的资源损耗,非核心线程创建执行完任务后会在一定时间后被销毁,这是需要消耗系统资源的

Java 线程池:当核心线程满了之后将新提交的任务放入等待队列中而非直接创建一个非核心线程执行任务,我认为这是基于“核心线程能够在较短的时间内完成任务并继续执行等待队列中的任务”这一想法的

你说的核心线程满了之后直接创建非核心线程去执行任务,可以参考 Tomcat 中的线程池

其中的考量也可能和任务类型有关,Tomcat 主要是 IO 密集型任务,Java 更多的是 CPU 密集型任务
future0906
149 天前
1. 可不可以这样设计:可以。
2. 为什么不这样设计:楼上已经回答你了,避免频繁创建销毁线程导致的 overhead ,尤其是边缘情况。
3. 我想要线程池这样设计:可以,请自行实现
4. 我反问楼主,如果耗时长的任务超过最大线程怎么办?线程池怎么知道每个任务的耗时?
cheng6563
149 天前
@zhangjiashu2023 你说的对,看来是我搞错了。那我也有和楼主一样的疑惑了。
Narcissu5
149 天前
《 Java Performace 》里面讲得很清楚了,计算密集型和 IO 密集型对线程池的要求是完全不同的,问题是线程池本身并不知道当前任务的特性,如果线程池里面是 IO 密集型,增加更多线程支持增加上下文切换的开销而已。这种方式是一种折中的方式,你需要自己根据任务特点设置参数
flyingpot
149 天前
不清楚为啥这么设计,不过 ES 使用了先加线程再放队列的线程池,只需要实现一个自定义的 queue 就行,类名叫 ExecutorScalingQueue
jimrok
148 天前
首先你的说法是没有问题的,这个问题是 java 的问题,如果你学习过其他语言,如 erlang ,它是可以马上创建新的线程去执行的。erlang 可以轻松并发几百万个线程。其实 erlang 这个不叫线程,叫协程。java 做不到是因为线程太昂贵了,这个资源创建时候,需要准备部分线程栈存放线程的元数据,大概要消耗一小部分内存,同时要映射到操作系统的线程上,如果经常这样做,开销是非常高的,可能会超过你执行线程的任务。所以,现在 java 的模式是做一个线程池,通过一个队列来接受任务,避免反复创建线程的开销。
jimrok
148 天前
Java 的这个线程池设计问题可以追朔到 java 语言创立之初的作用,最早 java 是想提供给智能硬件编程用的,没有考虑服务器大并发的使用场景,后续的 golang 还有 erlang 的并发模型都和 java 不一样,只能说 java 用在服务器上编程大大超出了创建者的预期。每年都有新的技术出现,开发者可能疲于追求新的技术,而忘记了探讨一项技术的本源是什么,上面太多的回答是从使用的角度解释的,可能从业这么长时间,也没有去了解过这些技术是怎么演进的。我想,可能参与这个过程的老师傅们已经下岗了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1036271

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX