面试官一个线程池问题把我问懵逼了。 (上)

简介: 面试官一个线程池问题把我问懵逼了。 (上)

这是why的第 98 篇原创文章

前几天,有个朋友在微信上找我。他问:why哥,在吗?

我说:发生肾么事了?

他啪的一下就提了一个问题啊,很快。

我大意了,随意瞅了一眼,这题不是很简单吗?

结果没想到里面还隐藏着一篇文章。

故事,得从这个问题说起:


2a7d0838d7439a85784a17ad8c1f01b.jpg


上面的图中的线程池配置是这样的:

ExecutorService executorService = new ThreadPoolExecutor(40, 80, 1, TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(100), 
                new DefaultThreadFactory("test"),
                new ThreadPoolExecutor.DiscardPolicy());

上面这个线程池里面的参数、执行流程啥的我就不再解释了。

毕竟我曾经在《一人血书,想让why哥讲一下这道面试题。》这篇文章里面发过毒誓的,再说就是小王吧了:

image.png


上面的这个问题其实就是一个非常简单的八股文问题:

非核心线程在什么时候被回收?

如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,就会被回收。

标准答案,完全没毛病。

那么我现在带入一个简单的场景,为了简单直观,我们把线程池相关的参数调整一下:

ExecutorService executorService = new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2), 
                new DefaultThreadFactory("test"),
                new ThreadPoolExecutor.DiscardPolicy());

那么问题来了:

  • 这个线程最多能容纳的任务是不是 5 个?
  • 假设任务需要执行 1 秒钟,那么我直接循环里面提交 5 个任务到线程池,肯定是在 1 秒钟之内提交完成,那么当前线程池的活跃线程是不是就是 3 个?
  • 如果接下来的 30 秒,没有任务提交过来。那么 30 秒之后,当前线程池的活跃线程是不是就是 2 个?

上面这三个问题的答案都是肯定的,如果你搞不明白为什么,那么我建议你先赶紧去补充一下线程池相关的知识点,下面的内容你强行看下去肯定是一脸懵逼的。

接下来的问题是这样的:

  • 如果当前线程池的活跃线程是 3 个(2 个核心线程+ 1 个非核心线程),但是它们各自的任务都执行完成了,都处于 waiting 状态。然后我每隔 3 秒往线程池里面扔一个耗时 1 秒的任务。那么 30 秒之后,活跃线程数是多少?

先说答案:还是 3 个。

从我个人正常的思维,是这样的:核心线程是空闲的,每隔 3 秒扔一个耗时 1 秒的任务过来,所以仅需要一个核心线程就完全处理的过来。

那么,30 秒内,超过核心线程的那一个线程一直处于等待状态,所以 30 秒之后,就被回收了。 但是上面仅仅是我的主观认为,而实际情况呢?

30 秒之后,超过核心线程的线程并不会被回收,活跃线程还是 3 个。 到这里,如果你知道是 3 个,且知道为什么是 3 个,即了解为什么非核心线程并没有被回收,那么接下里的内容应该就是你已经掌握的了。

可以不看,拉到最后,点个赞,去忙自己的事情吧。

如果你不知道,可以接着看,了解一下为什么是 3 个。

虽然我相信没有面试官会问这样的问题,但是对于你去理解线程池,是有帮助的。


先上 Demo


基于我前面说的这个场景,码出代码如下:

public class ThreadTest {
    @Test
    public void test() throws InterruptedException {
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2), new DefaultThreadFactory("test"),
                new ThreadPoolExecutor.DiscardPolicy());
        //每隔两秒打印线程池的信息
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("=====================================thread-pool-info:" + new Date() + "=====================================");
            System.out.println("CorePoolSize:" + executorService.getCorePoolSize());
            System.out.println("PoolSize:" + executorService.getPoolSize());
            System.out.println("ActiveCount:" + executorService.getActiveCount());
            System.out.println("KeepAliveTime:" + executorService.getKeepAliveTime(TimeUnit.SECONDS));
            System.out.println("QueueSize:" + executorService.getQueue().size());
        }, 0, 2, TimeUnit.SECONDS);
        try {
            //同时提交5个任务,模拟达到最大线程数
            for (int i = 0; i < 5; i++) {
                executorService.execute(new Task());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //休眠10秒,打印日志,观察线程池状态
        Thread.sleep(10000);
        //每隔3秒提交一个任务
        while (true) {
            Thread.sleep(3000);
            executorService.submit(new Task());
        }
    }
    static class Task implements Runnable {
        @Override
        public void run(){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "-执行任务");
        }
    }
}

这份代码也是提问的哥们给我的,我做了微调,你直接粘出去就能跑起来。

show me code,no bb。这才是相互探讨的正确姿势。

这个程序的运行结果是这样的:


image.png


一共五个任务,线程池的运行情况是什么样的呢?

先看标号为 ① 的地方:

三个线程都在执行任务,然后 2 号线程和 1 号线程率先完成了任务,接着把队列里面的两个任务拿出来执行(标号为 ② 的地方)。

按照程序,接下来,每隔 3 秒就有一个耗时 1 秒的任务过来。而此时线程池里面的三个活跃线程都是空闲状态。

那么问题就来了:

该选择哪个线程来执行这个任务呢?是随机选一个吗?

虽然接下来的程序还没有执行,但是基于前面的截图,我现在就可以告诉你,接下来的任务,线程执行顺序为:

  • Thread[test-1-3,5,main]-执行任务
  • Thread[test-1-2,5,main]-执行任务
  • Thread[test-1-1,5,main]-执行任务
  • Thread[test-1-3,5,main]-执行任务
  • Thread[test-1-2,5,main]-执行任务
  • Thread[test-1-1,5,main]-执行任务
  • ......

即虽然线程都是空闲的,但是当任务来的时候不是随机调用的,而是轮询。

由于是轮询,每三秒执行一次,所以非核心线程的空闲时间最多也就是 9 秒,不会超过 30 秒,所以一直不会被回收。

基于这个 Demo,我们就从表象上回答了,为什么活跃线程数一直为 3。


为什么是轮询?


我们通过 Demo 验证了上面场景中,线程执行顺序为轮询。

那么为什么呢?

这只是通过日志得出的表象呀,内部原理呢?对应的代码呢?

这一小节带大家看一下到底是怎么回事。

首先我看到这个表象的时候我就猜测:这三个线程肯定是在某个地方被某个队列存起来了,基于此,才能实现轮询调用。

所以,我一直在找这个队列,一直没有找到对应的代码,我还有点着急了。想着不会是在操作系统层面控制的吧?

后来我冷静下来,觉得不太可能。于是电光火石之间,我想到了,要不先 Dump 一下线程,看看它们都在干啥:

image.png

Dump 之后,这玩意我眼熟啊,AQS 的等待队列啊。

根据堆栈信息,我们可以定位到这里的源码:

java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#awaitNanos

image.png

看到这里的时候,我才一下恍然大悟了起来。

害,是自己想的太多了。

说穿了,这其实就是个生产者-消费者的问题啊。

三个线程就是三个消费者,现在没有任务需要处理,它们就等着生产者生产任务,然后通知它们准备消费。

由于本文只是带着你去找答案在源码的什么地方,不对源码进行解读。

所以我默认你是对 AQS 是有一定的了解的。

可以看到 addConditionWaiter 方法其实就是在操作我们要找的那个队列。学名叫做等待队列。

Debug 一下,看看队列里面的情况:

image.png

目录
相关文章
|
3月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
76 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
2月前
|
消息中间件 存储 前端开发
面试官:说说停止线程池的执行流程?
面试官:说说停止线程池的执行流程?
51 2
面试官:说说停止线程池的执行流程?
|
2月前
|
消息中间件 前端开发 NoSQL
面试官:如何实现线程池任务编排?
面试官:如何实现线程池任务编排?
33 1
面试官:如何实现线程池任务编排?
|
3月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
【多线程面试题 一】、 创建线程有哪几种方式?
创建线程的三种方式包括继承Thread类、实现Runnable接口和实现Callable接口,其中Runnable和Callable接口方式更受推荐,因为它们允许多重继承并更好地体现面向对象思想。
|
3月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
3月前
|
Java 调度
【多线程面试题 五】、 介绍一下线程的生命周期
线程的生命周期包括新建、就绪、运行、阻塞和死亡状态,线程状态会根据线程的执行情况在这些状态之间转换。
【多线程面试题 五】、 介绍一下线程的生命周期
|
3月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。