本文首发于个人微信公众号《andyqian》, 关注免费获取Java学习资料

前言

  多线程一直是Java进阶的必修课。在Java中,我们很早就知道可以通过 Thread 类和 Runnable 接口来实现多线程。与之有着类似职责的数据库连接,也可通过JDBC创建与使用。但我们深知无论是数据库连接的创建与销毁,还是线程的创建与销毁,都是一件及其消耗性能的事情。为了减少这种情况的发生,前辈们就在思考,是不是可以复用已有的数据库连接?减少创建,销毁动作?这就是后来数据库连接池的由来。同样的,为了复用线程,也就有了线程池。我一直独自暗喜,身为一位幸福的Java程序员,前有Java虚拟机管理内存,后有Doug Lea 大师提供并发库,锁机制。简直幸福的不像话,不过幸福归幸福,该掌握的还是需要掌握的,我们一起来看看今天的主角:ThreadPoolExecutor。

简介

  在面试过程中,也时常会遇到一些关于线程池的问题,例如:

  1. 线程池中核心参数有哪些?

  2. 有没有自己实现过线程池?

  3. 如果让你自己实现线程池,你会怎么做?

这些问题,其实考核的就是Java线程池的知识,更具体一点就是对ThreadPoolExector类熟不熟悉,下面代码是ThreadPoolExector类的全参构造函数。下面我们就一一对参数进行了解。

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

其中:

  1. corePoolSize:表示该线程池最小的工作线程数。默认情况下,当需要使用时创建线程,也可以调用 prestartAllCoreThreads() 方法进行预创建所有的核心线程。

  2. maximumPoolSize:表示该线程池最大的线程数量,理论上将其设置为无限大,就会创建无限多的线程,当然,创建线程的数量最终由系统资源也就是操作系统决定。

  3. keepAliveTime:表示空闲线程的超时时间,(单位为纳秒)。但在构造函数中,单位与unit 参数配合使用,最终转换为纳秒。

  4. unit:表示空闲线程超时的时间单位,可选值有:java.util.concurrent.TimeUnit中的值,SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天) 等。

  5. workQueue :表示工作队列(其实是一个runnable队列,在线程池中定义为Worker),其基类为:java.util.concurrent.BlockingQueue。

  6. threadFactory:线程工厂,通常用于创建线程,以及命令规则。默认为: Executors.defaultThreadFactory()。

  7. handler 表示处理策略,当workQueue队列满时,以及创建线程错误时的处理策略。其基类为 java.util.concurrent.RejectedExecutionHandler。默认为:AbortPolicy 策略。

不同组合

  在 java.util.concurrent.Executors类为我们提供了多种组合,其底层还是调用ThreadPoolExecutor。下面列举几个常用的方法:

1. newFixedThreadPool

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

特性:线程数量大小固定,且 corePoolSize 与 maximumPoolSize 数量相等。当线程数量设置太少时。task则会积压在LinkedBlockingQueue队列中。当 task 任务大于Integer.MAX_VALUE时 则会有OOM发生的风与之类是的还有newSingleThreadExecutor方法。

2. newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                   60L, TimeUnit.SECONDS,
                                   new SynchronousQueue<Runnable>());
 }

特性:corePoolSize数量为0,maximumPoolSize数量为Integer.MAX_VALUE,也就是说理论上是可以创建Integer.MAX_VALUE个线程的。keepAiveTime时间为 60秒。BlockingQueue使用的是SynchronousQueue,由于其没用容量,意味这每一次put对应着一次take操作,其吞吐量比较高。正因为如此,当task到达一定程度时,可能会创建许多线程,从而导致OOM,甚至服务不可用。

上述方法其实是对ThreadPoolExecutor方法的封装对不对,知道了ThreadPoolExecutor的每一个参数,再来使用这个,就得心应手了对不对。

规范

Executors类这么方便,是不是可以直接使用Executors类来创建呢?当然可以,但并不建议这样做。在《阿里Java手册》中的并发处理小节中有提到:

在实际应用中,我们应该遵守规范,避免掉一些没必要的问题。该规约其最终目的是让大家能够更清楚了解线程池的每个参数,从而达到能够在实际应用场景中调为最优组合使用,使其达到最大性能。同样的,我们也通过安装阿里巴巴的规约插件进行自动扫描与提醒。在Idea中 File -> Setings -> Plugins -> Browse repositories中搜索『Alibaba Java Coding Guidelines』安装即可!

小结

在这篇文章中,算是对ThreadPoolExector的一个初步了解。知道了其核心参数,到底是怎么回事。但这还并不够,且不足以学以致用,还有很多疑问,如:

  1. ThreadPoolExecutor的原理是怎样的?

  2. 我们如何自定义一个线程池?

  3. ThreadPoolExecutor在Dubbo中的实践

这些疑惑都需要一一去揭晓。由于篇幅原因,这些会作为好几篇文章进行记录。其目的是能够学以致用,面试时也能得心应手。


相关阅读:

使用 Mybatis 真心不要偷懒!

再谈Java 生产神器 BTrace

Java 生产神器  BTrace

重构不完全指南!


如果想深入学习Java并发编程,《Java并发编程的艺术》这本书是值得阅读的。当然了,如果你读喜欢电子书,也可以回复公众号消息『Java并发编程的艺术』进行免费获取!

Java线程池ThreadPoolExecutor-LMLPHP

关注免费获取Java学习资料

05-15 10:51