乐趣区

关于java:面试官一个线程池问题把我问懵逼了

这是 why 的第 98 篇原创文章

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

我说:产生肾么事了?

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

我粗心了,随便瞅了一眼,这题不是很简略吗?

后果没想到外面还暗藏着一篇文章。

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

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

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

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

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

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

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

如果通过 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。这才是互相探讨的正确姿态。

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

一共五个工作,线程池的运行状况是什么样的呢?

先看标号为 ① 的中央:

三个线程都在执行工作,而后 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 一下线程,看看它们都在干啥:

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

依据堆栈信息,咱们能够定位到这里的源码:

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

看到这里的时候,我才一下豁然开朗了起来。

害,是本人想的太多了。

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

三个线程就是三个消费者,当初没有工作须要解决,它们就等着生产者生产工作,而后告诉它们筹备生产。

因为本文只是带着你去找答案在源码的什么中央,不对源码进行解读。

所以我默认你是对 AQS 是有肯定的理解的。

能够看到 addConditionWaiter 办法其实就是在操作咱们要找的那个队列。学名叫做期待队列。

Debug 一下,看看队列外面的状况:

巧了嘛,这不是。程序刚好是:

  • Thread[test-1-3,5,main]
  • Thread[test-1-2,5,main]
  • Thread[test-1-1,5,main]

消费者这边咱们大略摸清楚了,接着去看看生产者。

  • java.util.concurrent.ThreadPoolExecutor#execute

线程池是在这里把工作放到队列外面去的。

而这个办法外面的源码是这样的:

其中 signalNotEmpty() 最终会走到 doSignal 办法,而该办法外面会调用 transferForSignal 办法。

这个办法外面会调用 LockSupport.unpark(node.thred) 办法,唤醒线程:

而唤醒的程序,就是期待队列外面的程序:

所以,当初你晓得当一个工作来了之后,这个工作该由线程池外面的哪个线程执行,这个不是随机的,也不是轻易来的。

是考究一个程序的。

什么程序呢?

Condition 外面的期待队列外面的程序。

什么,你不太懂 Condition?

那还不连忙去学?等着我给你讲呢?

原本我是想写一下的,起初发现《Java 并发编程的艺术》一书中的 5.6.2 大节曾经写的挺分明了,图文并茂。这部分内容其实也是面试的时候的高频考点,所以本人去看看就好了。

先欠着,欠着。

非核心线程怎么回收?

还是下面的例子,假如非核心线程就闲暇了超过 30 秒,那么它是怎么被回收的呢?

这个也是一个比拟热门的面试题。

这题没有什么浅近的中央,答案就藏在源码的这个中央:

  • java.util.concurrent.ThreadPoolExecutor#getTask

当 timed 参数为 true 的时候,会执行 workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) 办法。

而 timed 什么时候为 true 呢?

  • boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

allowCoreThreadTimeOut 默认为 false。

所以,就是看 wc > corePoolSize 条件,wc 是沉闷线程数。此时沉闷线程数为 3,大于外围线程数 2。

因而 timed 为 true。

也就是说,以后 workQueue 为空的时候,当初三个线程都阻塞 workQueue.poll 办法中。

而当指定工夫后,workQueue 还是为空,则返回为 null。

于是在 1077 行把 timeOut 批改为 true。

进入一下次循环,返回 null。

最终会执行到这个办法:

  • java.util.concurrent.ThreadPoolExecutor#processWorkerExit

而这个办法外面会执行 remove 的操作。

于是线程就被回收了。

所以当超过指定工夫后,线程会被回收。

那么被回收的这个线程是外围线程还是非核心线程呢?

不晓得。

因为在线程池外面,外围线程和非核心线程仅仅是一个概念而已,其实拿着一个线程,咱们并不能晓得它是外围线程还是非核心线程。

这个中央就是一个证实,因为当工作线程多余外围线程数之后,所有的线程都在 poll,也就是说所有的线程都有可能被回收:

另外一个强有力的证实就是 addWorker 这里:

core 参数仅仅是管制取 corePoolSize 还是 maximumPoolSize。

所以,这个问题你说怎么答复:

JDK 辨别的形式就是不辨别。

那么咱们能够晓得吗?

能够,比方通过观察日志,后面的案例中,我就晓得这两个是外围线程,因为它们最先创立:

  • Thread[test-1-1,5,main]- 执行工作
  • Thread[test-1-2,5,main]- 执行工作

在程序外面怎么晓得呢?

目前是不晓得的,然而这个需要,加钱就能够实现。

本人扩大一下线程池嘛,给线程池外面的线程打个标还不是一件很简略的事件吗?

只是你想想,你辨别这玩意干啥,有没有可落地的需要?

毕竟,脱离需要谈实现。都是耍流氓。

最初说一句

满腹经纶,难免会有纰漏,如果你发现了谬误的中央,能够在后盾提出来,我对其加以批改。

感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

退出移动版