⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 why技术 「why技术」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

你好呀,我是why哥。

前几天,有个朋友在微信上找我。他问: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 个非核心线程),但是它们各自的任务都执行完成了。然后我每隔 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 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 的等待队列啊。

先说明一下:由于本文只是带着你去找答案在源码的什么地方,不对源码进行解读。所以我默认你是对 AQS 是有一定的了解的。

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

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

图片

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

害,是自己想的太多了。

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

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

可以看到 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]-执行任务

在程序里面怎么知道呢?

这个就比较难了,其实我觉得这个信息并不重要吧?

什么,你加钱?

加钱,加钱可以实现。

自己扩展一下线程池嘛,给线程池里面的线程打个标还不是一件很简单的事情吗?

只是你想想,你区分这玩意干啥,有没有可落地的需求?

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

荒腔走板

图片

周末的时候收到了之前在网上买的一个头戴式蓝牙耳机,放在家里用。

本来我是想多选选、多看看、多纠结一下的。

但是当我刷购物 APP 的时候,突然魅族的这款耳机映入了我的眼睛里面。

在这之前,其实我完全不知道这款耳机的。然后点进去简单看了一下介绍,甚至都没看评论,就直接下单了。

不为别的,只是因为我曾经也是魅族的忠实用户。

但是下单的时候我就在想:这应该是我最后一次为魅族充值了吧。

我用的第一款智能手机就是从同学那里买来的魅族的 M8,快 10 年过去了,我至今都还记得当时第一眼看到它的惊艳。

一款手机,怎么能做的这么漂亮呢?

提前 M8,了解魅族的朋友、了解这款手机的朋友都会由衷的说一句:国货之光。

由于 M8 给我带来的良好体验,后来的 10 年间,我每次换手机都是魅族。

我真的喜欢魅族、喜欢 flyme 系统、喜欢 mBack。

直到后面,止步于 Pro 7,黄章的“出山之作”。我当时刚好换手机,果断的买了,用了。之后,我决定不再为魅族充值了。

在我心里,魅族创始人黄章,才是第一个打造情怀,买情怀的人。我这句话不是贬低,而是赞扬,我曾经就愿意为这个男人的情怀买单。

但是,不管怎么样,我还是希望黄章早日做出自己的“梦想机”,魅族重回巅峰。

毕竟,我看到耳机快递里面魅族科技的卡片时,内心还是有一丝波澜的。

毕竟,我曾经也是魅友。

说到这个耳机,是真不错。

我写文章的时候就带着这个耳机,不愧是 MP3 发家的魅族:

高音准,中音甜,低音劲,总之一句话,就是通透!

文章目录
  1. 1. 先上 Demo
  2. 2. 为什么是轮询?
  3. 3. 非核心线程怎么回收?
  4. 4. 荒腔走板