Java的线程池就像是一个花瓶容器。

而把任务提交给线程池就像是把小球塞进花瓶。

整个过程就像下面这个有趣的动画:

java 开启线程最大值 java最大线程数_ci

下面我们先来了解一下Java线程池的参数。

希望看完这篇文章后, 再提起线程池的时候, 你脑海首先出现的, 会是一个花瓶 : )

1 线程池的参数意义

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

别慌,我们可以把线程池的参数做成花瓶的参数,这样一来很多东西就不言自明了。

2 线程池的参数可视化

我们回到前面所说的花瓶。

java 开启线程最大值 java最大线程数_ci_02

这个花瓶由 瓶口 、 瓶颈 、 瓶身 三个部分组成。

这三个部分分别对应着线程池的三个参数:maximumPoolSize, workQueue,corePoolSize。

java 开启线程最大值 java最大线程数_Java_03

java 开启线程最大值 java最大线程数_ci_04

线程池里的线程,我用一个红色小球表示,每来一个任务,就会生成一个小球:

java 开启线程最大值 java最大线程数_ci_05

而核心线程,也就是正在处理中的任务,则用灰色的虚线小球表示 (目前第一版动画先这样简陋点吧......)

java 开启线程最大值 java最大线程数_java 开启线程最大值_06

于是画风就变成了这样,“花瓶”有这么几个重要的参数:

• corePoolSize=> 瓶身的容量
• maximumPoolSize=> 瓶口的容量
• keepAliveTime=> 红色小球的存活时间
• unit=> keepAliveTime的时间单位,比如分钟,小时等
• workQueue=> 瓶颈,不同类型的瓶颈容量不同
• threadFactory=> 你投递小球进花瓶的小手 (线程工厂)
• handler=> 线程池拒绝任务的处理策略,比如小球被排出瓶外

如果往这个花瓶里面放入很多小球时(线程池执行任务);

瓶身 (corePoolSize) 装不下了, 就会堆积到 瓶颈 (queue) 的位置;

瓶颈还是装不下, 就会堆积到 瓶口 (maximumPoolSize);

直到最后小球从瓶口溢出。

还记得上面提到的那一组实际参数吗,代表的花瓶大体上是如下图这样的:

java 开启线程最大值 java最大线程数_java 开启线程最大值_07

那么参数可视化到底有什么实际意义呢?


3 阿里的规范

首先我们来看阿里开发手册中对于 Java 线程池的使用规范:

java 开启线程最大值 java最大线程数_ci_08

为什么规范中提及的四种线程会导致OOM呢?

我们看看这四种线程池的具体参数,然后再用花瓶动画演示一下导致OOM的原因。

线程池FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());

我们关心的参数如下

corePoolSize:nThreads
mamximumPoolSize:nThreads
workQueue:LinkedBlockingQueue

FixedThreadPool表示的花瓶就是下图这样子:

java 开启线程最大值 java最大线程数_ci_09

线程池SingleThreadPool:

public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
         0L, TimeUnit.MILLISECONDS,
         new LinkedBlockingQueue<Runnable>()));
  }

我们关心的参数如下

corePoolSize:1
mamximumPoolSize:1
workQueue:LinkedBlockingQueue

SingleThreadPool表示的花瓶就是下图这样子:

java 开启线程最大值 java最大线程数_线程池_10


虽然两个线程池的样子没什么差异,但是这里我们发现了一个问题:

为什么 FixedThreadPool 和 SingleThreadPool 的 corePoolSize和mamximumPoolSize 要设计成一样的?

回答这个问题, 我们应该关注一下线程池的 workQueue 参数。

线程池FixedThreadPool和SingleThreadPool 都用到的阻塞队列 LinkedBlockingQueue。

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的源码注释中我们可以看到, 如果不指定队列的容量, 那么默认就是接近无限大的。

java 开启线程最大值 java最大线程数_ci_11

从动画可以看出, 花瓶的瓶颈是会无限变长的, 也就是说不管瓶口容量设计得多大, 都是没有作用的!

所以不管线程池FixedThreadPool和SingleThreadPool 的mamximumPoolSize 等于多少, 都是不生效的!

线程池CachedThreadPool

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

表示的花瓶就是下图这样子:

java 开启线程最大值 java最大线程数_线程池_12

这里我们由发现了一个问题:

为什么CachedThreadPool的mamximumPoolSize要设计成接近无限大的?

回答这个问题, 我们再看一下线程池CachedThreadPool的 workQueue 参数:SynchronousQueue。

SynchronousQueue

来看SynchronousQueue的源码注释:

A synchronous queue does not have any internal capacity, not even a capacity of one.

从注释中我们可以看到, 同步队列可以认为是容量为0。

所以如果mamximumPoolSize不设计得很大, 就很容易导致溢出。

java 开启线程最大值 java最大线程数_ci_13

但是瓶口设置得太大,堆积的小球太多,又会导致OOM(内存溢出)。

java 开启线程最大值 java最大线程数_ci_14

线程池ScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
  super(corePoolSize, Integer.MAX_VALUE, 
  0, NANOSECONDS,
  new DelayedWorkQueue());
}

我们关心的参数如下

corePoolSize:corePoolSize
mamximumPoolSize:Integer.MAX_VALUE
workQueue:DelayedWorkQueue

可以看到, 这里出现了一个新的队列 workQueue:DelayedWorkQueue

DelayedWorkQueue

DelayedWorkQueue 是无界队列, 基于数组实现, 队列的长度可以扩容到 Integer.MAX_VALUE。

同时ScheduledThreadPool的 mamximumPoolSize 也是接近无限大的。

可以想象得到,ScheduledThreadPool就是史上最强花瓶, 极端情况下长度已经突破天际了!

java 开启线程最大值 java最大线程数_ci_15

到这里, 相信大家已经明白, 为什么这四种线程会导致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;
    }

java 开启线程最大值 java最大线程数_java 开启线程最大值_16