写点什么

怎么才算掌握了 JDK 中的线程池

用户头像
AI乔治
关注
发布于: 2020 年 10 月 15 日
怎么才算掌握了JDK中的线程池

JDK 并发包下面的线程池是面试中经常被考查的点,之前我写过一篇ThreadPoolExecutor源码分析的文章。因为篇幅有限当时没说面试中常见的考查点和哪些点是应该掌握。那篇文章着实有点长,更合适用电脑看,结合源码看。今天,我来谈谈自己觉得 ThreadPoolExecutor 哪些点是应该掌握的,这些点应该掌握的点正是面试中经常被问的东西。现在抛出几个问题,如果你都能答上来,可以不用往下面看啦。

  1. ThreadPoolExecutor 中常用参数有哪些,作用是什么?任务提交后,ThreadPoolExecutor 会按照什么策略去创建线程用于执行提交任务?

  2. ThreadPoolExecutor 有哪些状态,状态之间流转是什么样子的?

  3. ThreadPoolExecutor 中的线程哪个时间点被创建?是任务提交后吗?可以在任务提交前创建吗?

  4. ThreadPoolExecutor 中创建的线程哪个时间被启动?

  5. ThreadPoolExecutor 竟然是线程池那么他是如何做到重复利用线程的?

  6. ThreadPoolExecutor 中创建的同一个线程同一时刻能执行多个任务吗?如果不能是通过什么机制保证 ThreadPoolExecutor 中的同一个线程只能执行完一个任务,才会机会去执行另一个任务?

  7. ThreadPoolExecutor 中关闲线程池的方法 shutdown 与 shutdownNow 的区别是什么?

  8. 通过 submit 方法向 ThreadPoolExecutor 提交任务后,当所有的任务都执行完后不调用 shutdown 或 shutdownNow 方法会有问题吗?

  9. ThreadPoolExecutor 有没有提供扩展点,方便在任务执行前或执行后做一些事情?

如果回答的上就 pass 吧,哈哈

ThreadPoolExecutor 参数有哪些与创建线程策略?

ThreadPoolExecutor 参数

  • corePoolSize 线程池中的核心线程数

  • mmaximumPoolSize 线程池中的最大线程数

  • keepAliveTime 当线程池中线程数量超过 corePoolSize 时,允许等待多长时间从 workQueue 中拿任务

  • unit keepAliveTime 对应的时间单位,为 TimeUnit 类。

  • workQueue 阻塞队列,当线程池中线程数超过 corePoolSize 时,用于存储提交的任务。

  • threadFactory 线程池采用,该线程工厂创建线程池中的线程。

  • handler 为 RejectedExecutionHandler,当线程线中线程超过 maximumPoolSize 时采用的,拒绝执行处理器。

创建线程策略



简单介绍一下,一个任务提交给线程池后,线程池创建线程来执行提交任务的流程。

1、当提交任务时线程池中的来用执行任务的线程数小于 corePoolSize(核心线程数),则线程池利用 ThreadFacory(线程工厂)创建线程用于执行提交的任务。否则执行第二 2 步。

2、当提交任务时线程池中的来用执行任务的线程数大于 corePoolSize(核心线程数),但 workQueue 没有满,则线程池会将提交的任务先保存在 workQueue(工作队列),等待线程池中的线程执行完其它已提交任务后会循环从 workQueue 中取出任务执行。否则执行第 3 步。

3、当提交任务时线程池中的来用执行任务大于 corePoolSize(核心线程数),且 workQueu 已满,但没有超过 maximunPoolSize(最大线程数),则线程池利用 ThreadFacory(线程工厂)创建线程用于执行提交的任务。否则执行 4。

4、当提交任务时线程池中的来用执行任务大于 maximunPoolSize,执行线程池中配置的拒绝策略(RejectedExecutionHanlder)。

所以在设置 ThreadPoolExecutor 的参数时一定要特别小心,不建议采用很大的 ArrayBlockQueue 或不限大小的 LinkedBlockQueue,同时 corePoolSize 也不应该设置过大。CUP 密集的任务的话可以设置小一点(CUP 数据+1 这种)避免不必要的上下文切换;而对于 IO 密集的任务则 corePoolSize 则可以设置的大一点,可以避免长时间 IO 等待而 CUP 却空闲。threadFactory 建议采用自己定义的,让其创建的线程容易区分,方便问题定位。


线程池有哪些状态,状态之间流转是什么样子的?

  • RUNNING:运行中,接收新的任务或处理队列中的任务。

  • SHUTDOWN:关闭,不再接收新的任务,但会处理队列中的任务值为 0。

  • STOP:停止,不再接收新的任务,也不处理队列中的任务,并中断正在处理的任务。

  • TIDYING:所有任务已结束队列大小为 0,转变 TIDYING 状态的线程将会执行 terminated()方法。

  • TERMINATED:结束 terminated()已被执行完。

状态流程如下图:



池程池中的线程哪个时间点被创建?

ThreadPoolExecutor 中的线程哪个时间点被创建?是任务提交后吗?可以在任务提交前创建吗?

一般在任务被提交后,线程池会利用线程工厂去创建线程,但当线程池中线程数已为 corePoolSize 时或 maxmumPoolSize 时不会。可以在任务提交前通过 prestartCoreThread 方法或 prestartAllCoreThreads 方法预先创建核心线程。具体可以参考这下这个图:



ThreadPoolExecutor 中创建的线程哪个时间被启动?

线程池中线程实现是在 addWorker 方法中被创建的,详见之前文章中 addWorker 方法分析。创建后完,该线程就被启动。线程池中被创建的线程被封装到了 Worker 对象中,而 Worker 类又实现了 Runnable 接口,线程池中的线程又引用了 worker。当线程被 start 后实际就有机会等待操作系统调度执行 Worker 类的 run 方法。

Worker(Runnable firstTask) {  setState(-1);   this.firstTask = firstTask; //创建的线程引用了worker  this.thread = getThreadFactory().newThread(this);}复制代码
复制代码

ThreadPoolExecutor 竟然是线程池那么他是如何做到重复利用线程的?

一旦线程池通过 ThreadFactory 创建好线程后,就会将创建的线程封装到了 Worker 对象中,同时启动该线程。新创建的线程会执行刚提交的任务,同时会不断地从 workerQueue 中取出任务执行。线程池的线程复用正是通过不断地从 workerQueue 中取出任务来执行达到的。源码分析见 runWorkers 方法分析。

ThreadPoolExecutor 中创建的同一个线程同一时刻能执行多个任务吗?

同时一时刻不能执行多个任务,只有一个任务执行完时才能去执行另一个任务。上面说到线程池中通过 ThreadFacory 创建的线程最后会被封装到 Worker 中,而该线程又引用了 Worker,start 线程后,任务其实是在 Worker 中的 run 方法中被执行,最终 run 又将任务执行代理给 ThreadPoolExecutor 的 runWorker 方法。

private final class Worker        extends AbstractQueuedSynchronizer        implements Runnable    {...}复制代码
复制代码

Worder 一方面实现了 Runnable,另一方面又继承了 AQS。通过实现 AQS,Worker 具有了排它锁的语义,每次在执行提交任务时都会先 lock 操作,执行完任务后再做 unlock 操作。正是这个加锁与解锁的操作,保证了同一个线程要执行完当前任务才有机再去执行另一个任务。

ThreadPoolExecutor 中关闲线程池的方法 shutdown 与 shutdownNow 的区别是什么?

shutdown 方法是将线程池的状态设置为 SHUTDOWN,此时新任务不能被提交(提交会抛出异常),workerQueue 的任务会被继续执行,同时线程池会向那些空闲的线程发出中断信号。空闲的线程实际就不没在执行任务的线程。如何被封装在 worker 里的线程能加锁,这里这个线程实现会就空闲的。下面是向空闲的线程发出中断信号源码。

 private void interruptIdleWorkers(boolean onlyOne) {        final ReentrantLock mainLock = this.mainLock;        mainLock.lock();        try {            for (Worker w : workers) {                Thread t = w.thread;                //w.tryLock()用于加锁,看线程是否在执行任务                if (!t.isInterrupted() && w.tryLock()) {                    try {                        t.interrupt();                    } catch (SecurityException ignore) {                    } finally {                        w.unlock();                    }                }                if (onlyOne)                    break;            }        } finally {            mainLock.unlock();        }    }复制代码
复制代码

shutdownNow 方法是将线程池的状态设置为 STOP,此时新任务不能被提交(提交会抛出异常),线程池中所有线程都会收到中断的信号。具体线程会作出什么响应,要看情况,如果线程因为调用了 Object 的 wait、join 方法或是自身的 sleep 方法而阻塞,那么中断状态会被清除,同时抛出 InterruptedException。其它情况可以参考 Thread.interrupt 方法的说明。shutdownNow 方法向所有线程发出中断信息源码如下:

private void interruptWorkers() {    final ReentrantLock mainLock = this.mainLock;    //加锁操作保证中断过程中不会新woker被创建    mainLock.lock();    try {         for (Worker w : workers)           w.interruptIfStarted();    } finally {          mainLock.unlock();    }}复制代码
复制代码

通过 submit 方法向 ThreadPoolExecutor 提交任务后,当所有的任务都执行完后不调用 shutdown 或 shutdownNow 方法会有问题吗?

如果没指核心线程允许超时将会有问题。核心线程允许超时是指在从 wokerQueue 中获取任务时,采用的阻塞的获取方式等待任务到来,还是通过设置超时的方式从同步阻塞队列中获取任务。即是通通过 BlockingQueue 的 poll 方法获取任务还是 take 方法获取任务。可参考之前的源码分析中的 getTask 方法分析。如果不调用 shutdown 或 shutdownNow 方法,核心线程由于在 getTask 方法调用 BlockingQueue.take 方法获取任务而处于一直被阻塞挂起状态。核心线程将永远处于 Blocking 的状态,导致内存泄漏,主线程也无法退出,除非强制 kill。试着运行如下程序会发现,程序无法退出。

public class Test {    public static void main(String args[]) {        ExecutorService executorService = new ThreadPoolExecutor(3, 3,10L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));        executorService.submit(new Runnable() {            @Override            public void run() {                System.out.println("thread name " + Thread.currentThread().getName());            }        });    }}复制代码
复制代码

所在使用线程池时一定要记得根本具体场景调用 shutdown 或 shutdownNow 方法关闭线程池。shutdown 方法适用于提交任务都要被执行完的场景,shutdownNow 方法适用于不关心提交任务是否执行完的场景。

ThreadPoolExecutor 有没有提供扩展点,方便在任务执行前或执行后做一些事情?

线程池提供了三个扩展点,分别是提交任务的 run 方法或是 call 方法被调用前与被调后,即 beforeExecutor 与 afaterExecutor 方法;另外一个扩展点是线程池的状态从 TIDYING 状态流转为 TERMINATED 状态时 terminated 方法会被调用。

总结

本来只是想写一点点,写着写着就发现又有点长。这篇主要是介绍了 ThreadPoolExecutor 中个人认为比较重要点,同时也是把 ThreadPoolExecutor 再梳理一下发现自己之前理解有偏差的地方。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java 烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章 ing🚀


作者:叶易

出处:club.perfma.com/article/191…


用户头像

AI乔治

关注

分享后端技术干货。公众号【 Java烂猪皮】 2019.06.30 加入

一名默默无闻的扫地僧!

评论

发布
暂无评论
怎么才算掌握了JDK中的线程池