赞
踩
Java的线程池就像是一个花瓶容器。
而把任务提交给线程池就像是把小球塞进花瓶。
整个过程就像下面这个有趣的动画:
下面我们先来了解一下Java线程池的参数。
希望看完这篇文章后, 再提起线程池的时候, 你脑海首先出现的, 会是一个花瓶 : )
Java线程池的构造函数如下:
- public ThreadPoolExecutor(
- int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- //...
- }
线程池有这么几个重要的参数:
corePoolSize=>
线程池里的核心线程数量maximumPoolSize
=> 线程池里允许有的最大线程数量keepAliveTime=>
空闲线程存活时间unit=>
keepAliveTime的时间单位,比如分钟,小时等workQueue=> 缓冲
队列threadFactory=>
线程工厂用来创建新的线程放入线程池handler=>
线程池拒绝任务的处理策略,比如抛出异常等策略线程池大体的原理就是这样的:corePoolSize ->queue -> maxPoolSzie , 吧啦吧啦......
那么现在重点来了, 这堆参数解释不看源码真的搞不懂怎么办?
或者你看懂了这些参数的文字解析,但是到用的时候总是记不住怎么办?
或者我们来一组实际参数,你能理解这代表的含义吗?
- corePoolSize:1
- mamximumPoolSize:3
- keepAliveTime:60s
- workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
- handler:默认的策略,抛出来一个ThreadPoolRejectException
别慌,我们可以把线程池的参数做成花瓶的参数,这样一来很多东西就不言自明了。
我们回到前面所说的花瓶。
这个花瓶由 瓶口 、 瓶颈 、 瓶身 三个部分组成。
这三个部分分别对应着线程池的三个参数:maximumPoolSize, workQueue,corePoolSize。
线程池里的线程,我用一个红色小球表示,每来一个任务,就会生成一个小球:
而核心线程,也就是正在处理中的任务,则用灰色的虚线小球表示 (目前第一版动画先这样简陋点吧......)
于是画风就变成了这样,“花瓶”有这么几个重要的参数:
如果往这个花瓶里面放入很多小球时(线程池执行任务);
瓶身 (corePoolSize) 装不下了, 就会堆积到 瓶颈 (queue) 的位置;
瓶颈还是装不下, 就会堆积到 瓶口 (maximumPoolSize);
直到最后小球从瓶口溢出。
还记得上面提到的那一组实际参数吗,代表的花瓶大体上是如下图这样的:
那么参数可视化到底有什么实际意义呢?
首先我们来看阿里开发手册中对于 Java 线程池的使用规范:
为什么规范中提及的四种线程会导致OOM呢?
我们看看这四种线程池的具体参数,然后再用花瓶动画演示一下导致OOM的原因。
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
我们关心的参数如下
- corePoolSize:nThreads
- mamximumPoolSize:nThreads
- workQueue:LinkedBlockingQueue
FixedThreadPool表示的花瓶就是下图这样子:
- public static ExecutorService newSingleThreadExecutor() {
- return new FinalizableDelegatedExecutorService
- (new ThreadPoolExecutor(1, 1,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>()));
- }
我们关心的参数如下
- corePoolSize:1
- mamximumPoolSize:1
- workQueue:LinkedBlockingQueue
SingleThreadPool表示的花瓶就是下图这样子:
虽然两个线程池的样子没什么差异,但是这里我们发现了一个问题:
为什么 FixedThreadPool 和 SingleThreadPool 的 corePoolSize和mamximumPoolSize 要设计成一样的?
回答这个问题, 我们应该关注一下线程池的 workQueue 参数。
线程池FixedThreadPool和SingleThreadPool 都用到的阻塞队列 LinkedBlockingQueue。
The capacity, if unspecified, is equal to {@link Integer#MAX_VALUE}. Linked nodes are dynamically created upon each insertion unless this would bring the queue above capacity.
从LinkedBlockingQueue的源码注释中我们可以看到, 如果不指定队列的容量, 那么默认就是接近无限大的。
从动画可以看出, 花瓶的瓶颈是会无限变长的, 也就是说不管瓶口容量设计得多大, 都是没有作用的!
所以不管线程池FixedThreadPool和SingleThreadPool 的mamximumPoolSize 等于多少, 都是不生效的!
- public static ExecutorService newCachedThreadPool() {
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>());
我们关心的参数如下
- corePoolSize:0
- mamximumPoolSize:Integer.MAX_VALUE
- workQueue:SynchronousQueue
表示的花瓶就是下图这样子:
这里我们由发现了一个问题:
为什么CachedThreadPool的mamximumPoolSize要设计成接近无限大的?
回答这个问题, 我们再看一下线程池CachedThreadPool的 workQueue 参数:SynchronousQueue。
来看SynchronousQueue的源码注释:
A synchronous queue does not have any internal capacity, not even a capacity of one.
从注释中我们可以看到, 同步队列可以认为是容量为0。
所以如果mamximumPoolSize不设计得很大, 就很容易导致溢出。
但是瓶口设置得太大,堆积的小球太多,又会导致OOM(内存溢出)。
- public ScheduledThreadPoolExecutor(int corePoolSize) {
- super(corePoolSize, Integer.MAX_VALUE,
- 0, NANOSECONDS,
- new DelayedWorkQueue());
- }
我们关心的参数如下
- corePoolSize:corePoolSize
- mamximumPoolSize:Integer.MAX_VALUE
- workQueue:DelayedWorkQueue
可以看到, 这里出现了一个新的队列 workQueue:DelayedWorkQueue
DelayedWorkQueue 是无界队列, 基于数组实现, 队列的长度可以扩容到 Integer.MAX_VALUE。
同时ScheduledThreadPool的 mamximumPoolSize 也是接近无限大的。
可以想象得到,ScheduledThreadPool就是史上最强花瓶, 极端情况下长度已经突破天际了!
到这里, 相信大家已经明白, 为什么这四种线程会导致OOM了。
怎么感觉这四种线程还真是名副其实的“花瓶”呢 :)
目前花瓶动画还只是粗略的版本, 有部分瑕疵是不可避免的, 根据二八定律, 我的主要想法大体上是先做出来了,剩下的细节再慢慢补。
目前只体现了线程池的三个参数。
如果现在加入参数 keepAliveTime, 那么动画又会有什么效果的呢?
敬请期待后续更新的文章。
有很多人或许会认为, 学习个线程池, 还要做什么动画, 这不是走偏了吗?
引用大神的一句话回答这个问题:
Data visualization knowledge is not necessary -- just the desire to spread some knowledge.
—— Ben Johnson
数据可视化确实不是必需的, 但是有时候我们仅仅只是渴望给大家分享一些知识。
而且在这个分享的过程中, 动画会让你做出更多的思考:
思考动画怎么才能符合真实场景的效果。
比如当我们开始思考,动画中花瓶颈部的长度变化,以及DelayedWorkQueue队列容量的变化,这两者如何才能对应的上时,于是不可避免的, 我们会开始研究起DelayedWorkQueue的扩容方式。
甚至每一种队列都可以单独展开做成更加细化的动画。
而想要做好这些动画, 又要开始研究不同队列的源码了, 有需求才有动力!
- /**
- * 简单配置示例
- */
-
- //获取当前机器的核数
- public static final int cpuNum = Runtime.getRuntime().availableProcessors();
-
- @Override
- public Executor getAsyncExecutor() {
- ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
- taskExecutor.setCorePoolSize(cpuNum);//核心线程大小
- taskExecutor.setMaxPoolSize(cpuNum * 2);//最大线程大小
- taskExecutor.setQueueCapacity(500);//队列最大容量
- //当提交的任务个数大于QueueCapacity,就需要设置该参数,但spring提供的都不太满足业务场景,可以自定义一个,也可以注意不要超过QueueCapacity即可
- taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
- taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
- taskExecutor.setAwaitTerminationSeconds(60);
- taskExecutor.setThreadNamePrefix("BCarLogo-Thread-");
- taskExecutor.initialize();
- return taskExecutor;
- }
-
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。