why技术

对不起,五年前,我写错了一道面试题。但是此刻,我突然开悟了...

你好呀,我是歪歪。

事情是这样的,上周有个读者找我,给我抛出了这样的一个问题:

Image

问题中涉及到的文章分别是这两篇:

我自己写的这篇文章,虽然是五年前,2019 年的文章:

(卧槽,2019 年已经是五年前了)

Image

但是毕竟是自己一个字一个字敲出来的,大概内容还是记得。

主要就是讨论了我在面试的时候遇到的这个问题:

一个线程池中的线程异常了,那么线程池会怎么处理这个线程?

当时我的回答是这样的:

Image

在文章里面,我把我的回答总结成了三句话:

  • 1.抛出堆栈异常  ---这句话对了一半!
  • 2.不影响其他线程任务 ---这句话全对!
  • 3.这个线程会被放回线程池---这句话全错!

然后我的文章就基于上面这三句话展开了。

过程就不再赘述了,这次只讨论我五年前的文章中说错的一个点:这个(异常的)线程会被放回线程池。

当时我的结论是这句话全错了,正确的描述应该是:

(当一个线程池里面的线程异常后,)线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。

Image

对于同样的问题,京东技术的结论是这样的:

Image
  • 当执行方式是 execute 时,可以看到堆栈异常的输出,线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。
  • 当执行方式是 submit 时,堆栈异常没有输出。但是调用 Future.get() 方法时,可以捕获到异常,不会把这个线程移除掉,也不会创建新的线程放入到线程池中。

歪师傅的结论是一概而论,京东技术则是分情况讨论。

首先,京东技术的结论是正确的。

其次,歪师傅当年写这个文章的时候,就是技不如人,就是写错了,就是情况没有分析完整。

Image

只看了 execute 的情况,导致得出了一个“只对了一半的答案”。

而关于使用 submit 方法时,如果在线程中抛出了异常,为什么不创建新的线程,而是继续复用原线程的原因,京东技术也从源码的角度解析了。

歪师傅这里也赘述一下。

问题的关键就是要抓到关键的问题。

那么在这个问题中,关键的问题是什么?

就是移除线程的方法在哪儿。

对应到源码其实就是这里:

java.util.concurrent.ThreadPoolExecutor#processWorkerExit

Image

那么其实关键点就是这个方法在哪儿,在什么情况下会被调用到?

对应的源码在这里:

java.util.concurrent.ThreadPoolExecutor#runWorker

Image

通过源码我们可以知道,在抛出异常的情况下,该方法会被调用到。

而 try 部分就只有一行代码:

task.run();

那么能耍花招的地方就只能是 task 这个对象了。

比如这样的代码,当 execute 方法执行的时候,这就是一个原生的 Thread 线程:

Image

该方法是否会抛出异常,取决于你代码是否会抛出异常。

比如这样去写,线程执行 sayHi 方法的时候就会抛出异常:

Image

而这样去写,则不会抛出异常:

Image

所以,你再去看京东技术的结论:

execute 提交到线程池的方式,如果执行中抛出异常,并且没有在执行逻辑中 catch,那么会抛出异常,并且移除抛出异常的线程,创建新的线程放入到线程池中。

特别提到了 catch。

但是 submit 的时候,是怎么回事呢?

task 从一个普通线程变成了 FutureTask 对象:

Image

因为源码在这里玩个了个小花招:

java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable)

Image

把 task 包装成了 FutureTask 对象。

而一切的秘密就藏在 FutureTask 对象的 run 方法中:

java.util.concurrent.FutureTask#run

Image

异常之后,会调用 setException 方法,仅仅是把异常放在了 outcome 字段中,然后维护了 FutureTask 的状态,不会继续往外抛出异常。

如果需要获取异常,则需要调用 get 方法。

好,现在我要开始闭环了。

因为 submit 提交的时候会把任务封装为 FutureTask 对象,该对象重写了 run 方法,所以当任务异常之后,不会继续往外抛出异常。

因为不会继续往外抛出异常,所以不会走到 processWorkerExit 方法。

因为不会走到 processWorkerExit 方法,所以不涉及移除线程和添加线程的逻辑。

所以:

当执行方式是 submit 时,不会把这个线程移除掉,也不会创建新的线程放入到线程池中。

其实整体逻辑还是很清楚的,当年就是分析漏了 submit 的情况,导致最终的结论不对。

五年前我挖了个坑,五年后,我把这个坑填一下。

Image

然后再回答一个京东技术那篇文章下留言区的一个问题:

Image

execute 执行无论是否抛出异常,finally 块中代码不是都会执行吗?

也就是这段代码:

Image

如果你只看这部分 try 和 finally 代码块,我们学习 Java 的时候,如果老师没有骗我们的话,那么不管是正常执行完成 try 里面的代码,还是 try 里面的代码抛出异常, finally 代码块的代码理论上都是会执行的。

是的,这一个知识点没有任何毛病。

但是,你注意我是怎么说的“不管是正常执行完成还是抛出异常”。

抛出异常我们前面已经分析了,提问者的疑问点在于“正常执行完成”为什么不会执行 finally 代码块里面的 processWorkerExit 方法。

我的答案是:会。

但是,try 里面要正常执行完成,也就是 while 循环要正常结束,所以你看看一眼循环条件中的这个部分,要返回 null 才满足条件:

Image

getTask 对应的源码是这样的:

java.util.concurrent.ThreadPoolExecutor#getTask

Image

在我们讨论的场景下,线程是会阻塞在队列的 poll 或者 take 方法这里的。

如果是 take 方法就不说了,不会返回 null,在这里死等。

如果是 poll 方法返回了 null,则说明该线程到了超时时间还未从队列中获取到任务。

这个时候该怎么办?

翻翻八股文看看,如果线程池设置了 allowCoreThreadTimeOut 为 true,针对核心线程,在指定时间内未获取到任务或者非核心线程在指定时间内未获取到任务的时候,线程池会怎么处理?

是不是说的该销毁了,该从线程池中移走了?

所以,才会走到 processWorkerExit 执行 workers.remove(w) 方法。

是不是感觉自己又能行了,知识点又串起来了。

Image

一点思考

当读者问我“是复用还是移除”这个问题的时候,我当时确实不知道答案。

但是我一点都不慌,因为我知道去哪里找答案。

如果我真的需要想要知道答案的话,在不借助任何搜索工具,仅仅给我源码的情况下,我应该很快就能得到一个准确的答案。

这一点自信的底气是因为我确实较为深入的研究过这部分源码。

Image

但是当时我没有去寻找答案,结合我对于线程池的理解,我在思考另外一个问题:这重要吗?

你仔细想一想,如果这个问题抛出来之后你直接就是一头雾水,或者说和我一样知道去哪里找答案,那么这个问题的准确回答对你来说真的重要吗?

不管是那种情况都不重要,一点都不重要。

因为不管是销毁还是复用,它完全不影响你对于线程池的使用。

重要的是,在一头雾水的情况下,自己去寻找问题的答案的这个过程。

你当然可以拿着关键字去网上搜,肯定能搜到答案,这是一个寻找的过程,不过是轻松一点,然后遗忘起来快一点。

你也可以带着问题去翻源码,这也是一个寻找的过程,不过是难一点而已,记忆深刻一点。

如果觉得直接啃源码啃不动,那就结合网上的资料一起食用,这同样是一个寻找的过程。

等你真的找到这个问题的标准答案的时候、等你进一步理解线程池的时候,你会发现这个问题的答案不重要,但是在寻找的过程中你写的 Demo、接触到的源码、方法之间的调用关系、分支判断逻辑、查阅到的资料、付出的时间和对应的收获、甚至是内心中转瞬即逝的开心...

这些是重要的。

这个题其实是一个陷阱。

就像是我们读书的时候做的数学题,我们都知道参考答案就在练习册的最后几页,照着参考答案抄就能回答正确。

但是我们都知道比起正确答案来说,更重要的是你知道解题的过程。

最可怕的情况是你抄答案的次数多了,对自己产生了错误的认知,让你在抄答案的过程中还产生了这题很简单,自己也会做的错觉。

只有见过了无数千奇百怪的题目,摸熟了无数个解题的套路,当你在这个过程中,在某个瞬间体会到了“万变不离其宗”的时候,在自信心经历过建立、崩塌、再建立的过程后,在把参考答案真的只是当做参考的时候,你就可以淡定的说出:哦,这题啊,我没见过,但是我知道怎么去做。

就像是五年前我拿到这个题的时候,我经过一番研究,还是答错了。

五年后,再次遇到这个题的瞬间,我还是不知道答案,但是我的内心一点都不慌。

在学习编程的路上,这样的“陷阱题”真的太多太多了,难的不是回答出你被背下的标准答案,难的是你知道标准答案是怎么来的。

这就是我从“是复用还是移除”这个问题带给我的思考。

我觉得我其实是在试图给你阐述一种学习的方法,因为我也没有悟透,所以总感觉有点词不达意,但是我想要表述的都说完了,剩下的,我自己接着悟吧。

Image

合订本

翻了一下,我过往还是写了很多线程相关的文章的。

都放在这里,作为一个合订版吧:

《有的线程它死了,于是它变成一道面试题》

《关于多线程中抛异常的这个面试题我再说最后一次!》

《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》

《填个坑!再谈线程池动态调整那点事。》

《每天都在用,但你知道 Tomcat 的线程池有多努力吗?》

《这个队列的思路真的好,现在它是我简历上的亮点了。》

《虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。》

《面试官:你给我说一下线程池里面的几把锁。》

《Dubbo 2.7.5在线程模型上的优化》

《面试官问我知不知道异步编程的Future。》

《面试官问我知不知道CompletionService?》

《1000 多个并发线程,10 台机器,每台机器 4 核,设计线程池大小。》

《要我说,多线程事务它必须就是个伪命题!》

《Doug Lea在J.U.C包里面写的BUG又被网友发现了。》

《“借助同步”这个理念在 FutureTask 里面的应用。》

《面试官:Java如何绑定线程到指定CPU上执行?》

《别问了,我真的不喜欢 @Asyn 这个注解!》

《看完JDK并发包源码的这个性能问题,我惊了!》

《什么是高并发下的请求合并?》

《CompletableFuture 的那点事儿》

《看起来是线程池的BUG,但是我认为是源码设计不合理。》

《喜提JDK的BUG一枚!多线程的情况下请谨慎使用这个类的stream遍历。》

《听我一句劝,业务代码中,别用多线程。》

《面试官:一个 SpringBoot 项目能处理多少请求?(小心有坑)》

《线程池参数千万不要这样设置》

《刺激,线程池的一个BUG直接把CPU干到100%了。》

《这里有线程池、局部变量、内部类、静态嵌套类和一个莫得名堂的引用,哦,还有一个坑!》

《看到一个魔改线程池,面试素材加一!》

《面试官一个线程池问题把我问懵逼了。》

如果里面的某一篇曾经帮助过你,安排一个一键三连就行了。

本文的技术部分就到这里了。

下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。

荒腔走板

Image

在学习编程的路上,这样的“陷阱题”真的太多太多了,难的不是回答出你被背下的标准答案,难的是你知道标准答案是怎么来的。

我从小学三年级就开始戴眼镜了,可以说几乎是从我有记忆开始我就一直戴着眼镜。

早期的时候度数还没那么高,不上课的时候还是不愿意戴眼镜,因为那个时候近视的同学似乎特别少,戴眼镜,就显得和别的同学不一样。

但是不管我带不戴眼镜,他们就会叫我“王眼镜儿”,对于这个别名我从最开始的抵触到最后的无所谓,大概也就花了半个学期就接受了。

接受了之后就开始一直戴着眼镜了,从小学到现在,算了一下时间,刚好 20 年。

这 20 年间,随着读的书越多,度数也涨的越高,现在稳定在 900 度,属于非常高度的近视了。

我曾经自己量过,取下眼镜之后把一本书放在眼前,超过 17cm 就看不清楚上面的字了,所以没有眼镜约等于生活不能自理。

所以如果不借助于手机拍照,我根本不知道自己取下眼镜长什么样子。

但是前段时间鬼使神差的,我取下眼镜,拿着手机,对着镜子,站在很远的地方对着自己拍了一张全身照。

然后戴上眼镜,看着照片,那种感觉非常的神奇,第一反应就是说:这是谁?这是我?

我真的没有夸张,20 年的力量,已经让眼镜变成了我脸上一个器官般的存在,所以戴上眼镜和不戴眼镜区分非常的大,大到我看照片的时候有一种不真实的感觉。

那种不真实的感觉,让我产生了想要亲眼看看的想法。

于是我想到了隐形眼镜。

我之前是默认我的度数太高,肯定是不能戴隐形眼镜的,同时我也特别惧怕这个东西,因为我的眼睛已经够脆弱了,不能再受伤了,这样的想法甚至让我抵触隐形眼镜。

做了充分的攻略之后,发现其实我也可以以体验为目的,试着戴一下隐形眼镜。

于是上个周末,我买了五副,让 Max 同学帮我戴,戴了很长的时间,在我尽力克服心理障碍,全力配合的情况下,在报废了三副之后,终于在倒数第二次机会的时候戴进去了第一只隐形眼镜,是右边的眼睛。

于是我闭着左眼开始观察这个世界。

说真的,在我近三十年的人生体验中,第一次不戴眼镜就能这么清楚的看到这个世界,那么远都能看得那么清楚的时候,那一刻我突然好感度,有一点想哭的冲动。

那一天,我戴了大概四个小时的隐形眼镜,还去逛了商场。

在商场的一个巨大的落地镜子前,我又看到了镜子里面那个自己,很陌生,第一眼还以为是别的路人,似乎还需要一点时间才能适应这真的是我,真是一种神奇的体验。

我也是鼓起勇气才获得这样神奇的体验。

所以,那句话是怎么说的来着?

勇敢的人,先享受世界。

··············  END  ··············

你好呀,我是歪歪。我没进过一线大厂,没创过业,也没写过书,更不是技术专家,所以也没有什么亮眼的title。

当年高考,随缘调剂到了某二本院校计算机专业。纯属误打误撞,进入程序员的行列,之后开始了运气爆棚的程序员之路。

说起程序员之路还是有点意思,可以点击蓝字,查看我的程序员之路。