这是why的第 69 篇原创文章html
一个编号为 8073704 的 JDK BUG,将串联起个人这篇文章。java
也就是下面的这个连接。c++
https://bugs.openjdk.java.net/browse/JDK-8073704
面试
这个 BUG 在 JDK 9 版本中进行了修复。也就是说,若是你用的 JDK 8,也许会遇到这样的问题。segmentfault
先带你们看看这个问题是怎么样的:api
这个 BUG 说:FutureTask.isDone 方法在任务尚未完成的时候就会返回 true。并发
能够看到,这是一个 P4 级别(优先级不高)的 BUG,这个 BUG 也是分配给了 Doug Lea,由于 FutureTask 类就是他写的:oracle
响应了国家政策:谁污染,谁治理。less
这个 BUG 的做者 Martin 老哥是这样描述的:svg
下面我会给你们翻译一下他要表达的东西。
可是在翻译以前,我得先作好背景铺垫,以避免有的朋友看了后一脸懵逼。
若是要懂他在说什么,那我必须得再给你看个图片,这是 FutureTask 的文档描述:
看 Martin 老哥提交的 BUG 描述的时候,得对照着状态图和状态对应的数字来看。
他说 FutureTask#isDone 方法如今是这样写的:
他以为从源码来看,是只要当前状态不等于 NEW(即不等于0)则返回 true,表示任务完成。
他以为应该是这样写:
这样写的目的是除了判断了 NEW 状态以外,还判断了两个中间状态:COMPLETING 和 INTERRUPTING。
那么除去上面的三个状态以外呢,就只剩下了这四个状态:
这四个状态能够表明一个任务的最终状态。
固然,他说上面的代码还有优化空间,好比下面这样,代码少了,可是理解起来也得多转个弯:
state>COMPLETING,知足这个条件的状态只有下面这几种:
而这几种中,只有 INTERRUPTING 是一个中间态,因此他用后面的 != 排除掉了。
这样就是代码简洁了,可是理解起来多转个小弯。可是这两段代码表示的含义是如出一辙的。
好了,关于这个 BUG 的描述就是这样的。
汇总为一句话就是,这个 Martin 老哥认为:
FutureTask.isDone 方法在任务尚未完成的时候,好比仍是 COMPLETING 和 INTERRUPTING 的时候就会返回 true,这样是不对的。这就是 BUG。
仅从 isDone 源码中那段 status != NEW 的代码,我认为这个 Martin 老哥说的确实没有问题。由于确实有两个中间态,这段源码中是没有考虑的。
接下来,咱们就围绕着这个问题进行展开,看看各位大神的讨论。
首先,第一个发言的哥们是 Pardeep,是在这个问题被提出的 13 天以后:
我没有太 get 到这个哥们回答的点是什么啊。
他说:咱们应该去看一下 isDone 方法的描述。
描述上说:若是一个任务已完成,调用这个方法则返回true。而完成除了是正常完成外,还有多是任务异常或者任务取消致使的完成,这些都算完成。
我以为他的这个回答和问题有点对不上号,感受是答非所问。
就当他抛出了一个关于 isDone 方法的知识点吧。
三天后,第二个发言的哥们叫作 Paul,他的观点是这样的:
首先,他说咱们不须要检查 INTERRUPING 这个中间状态。
由于若是一个任务处于这个状态,那么获取结果的时候必定是抛出 CancellationException。
叫咱们看看 isCancelled 方法和 get 方法。
那咱们先看看 isCancelled 方法:
直接判断了状态是否大于等于 CANCELLED,也就是判断了状态是不是这三种中的一个:
判断任务是否取消(isCancelled)的时候,并无对 INTERRUPING 这个中间状态作特殊处理。
按照这个逻辑,那么判断任务是否完成(isDone)的时候,也不须要对 INTERRUPING 这个中间状态作特殊处理。
接着,咱们看看 get 方法。
get 方法最终会调用这个 report 方法:
若是变量 s (即状态)是 INTERRUPING (值是 5),那么是大于 CANCELLED (值是 4)状态的,则抛出 CancellationException (CE)异常。
因此,他以为对于 INTERRUPING 状态没有必要进行检测。
由于若是你调用 isCancelled 方法,那么会告诉你任务取消了。
若是你调用 get 方法,会抛出 CE 异常。
因此,综上所述,我认为 Paul 这个哥们的逻辑是这样的:
咱们做为使用者,最终都会调用 get 方法来获取结果,假设在调用 get 方法以前。咱们用 isCancelled 或者 isDone 判断了一下任务的状态。
若是当前状态好死不死的就是 INTERRUPING 。那么调用 isCancelled 返回 true,那按照正常逻辑,是不会继续调用 get 方法的。
若是调用的是 isDone ,那么也返回 true,就会去调用 get 方法。
因此在 get 方法这里保证了,就算当前处于 INTERRUPING 中间态,程序抛出 CE 异常就能够了。
所以,Paul 认为若是没有必要检测 INTERRUPING 状态的话,那么咱们就能够把代码从:
简化为:
可是,这个哥们还说了一句话来兜底。
他说:Unless i have missed something subtle about the interactions
除非我没有注意到一些很是小的细节问题。你看,说话的艺术。话都被他一我的说完了。
好了,Paul 同窗发言完毕了。42 分钟以后,一个叫 Chris 的小老弟接过了话筒,他这样说的:
我以为吧,保罗说的挺有道理的,我同意他的建议。
可是吧,我也以为咱们在讨论的是一个很是细节,很是小的问题,我不知道,就算如今这样写,会致使任何问题吗?
写到这里,先给你们捋一下:
因而他们以为 isDone 方法应该修改为这样:
因此,如今只剩下一个中间状态是有争议的了:COMPLETING 。
对于剩下的这个中间状态,一位叫作 David 的靓仔,在三小时后发表了本身的意见:
他上来就是一个暴击,直截了当的说:我认为在座的各位都是垃圾。
好吧,他没有这样说。因此你看,仍是要多学学英语,否则我骗了你,你还不知道。
其实他说的是:我认为没有必要作任何改变。
COMPLETING 状态是一个转瞬即逝的过渡状态,它表明咱们已经有最终状态了,可是在设置最终状态开始和结束的时间间隙内有一个瞬间状态,它就是 COMPLETING 状态。
其实你是能够经过 get 方法知道任务是不是完成了,经过这个方法你能够得到最终的正确答案。
由于 COMPLETING 这个转瞬即逝的过渡状态是不会被程序给检测到的。
David 靓仔的回答在两个半小时候获得了大佬的确定:
Doug Lea 说:如今源码里面是故意这样写的,缘由就是 David 这位靓仔说的,我写的时候就是这样考虑过的。
另外,我以为这个 BUG 的提交者本身应该解释咱们为何须要修改这部分代码。
其实 Doug 的言外之意就是:你说这部分有问题,你给我举个例子,别只是整理论的,你弄点代码给我看看。
半小时以后,这个 BUG 的提交者回复了:
intentional 知道是啥意思不?
害,我又得兼职教英语了:
他说:哦,原来是故意的呀。
这句话,你用不一样的语气能够读出不一样的含义。
我这里倾向于他以为既然 Doug 当初写这段代码的时候考虑到了这点,他分析以后以为本身这样写是没有问题的,就这样写了。
好嘛,前面说 INTERRUPING 不须要特殊处理,如今说 COMPLETING 状态是检测不到的。
那就没得玩了。
事情如今看起来已是被定性了,那就是不须要进行修改。
可是就在这时 Paul 同窗杀了个回马枪,应该也是前面的讨论激发了他的思路,你不是说检测不出来吗,你不是说 get 方法能够得到最终的正确结果吗?
那你看看我这段代码是什么状况:
代码是这样的,你们能够直接粘贴出来,在 JDK 8/9 环境下分别运行一下:
public static void main(String[] args) throws Exception { AtomicReference<FutureTask<Integer>> a = new AtomicReference<>(); Runnable task = () -> { while (true) { FutureTask<Integer> f = new FutureTask<>(() -> 1); a.set(f); f.run(); } }; Supplier<Runnable> observe = () -> () -> { while (a.get() == null); int c = 0; int ic = 0; while (true) { c++; FutureTask<Integer> f = a.get(); while (!f.isDone()) {} try { /* Set the interrupt flag of this thread. The future reports it is done but in some cases a call to "get" will result in an underlying call to "awaitDone" if the state is observed to be completing. "awaitDone" checks if the thread is interrupted and if so throws an InterruptedException. */ Thread.currentThread().interrupt(); f.get(); } catch (ExecutionException e) { throw new RuntimeException(e); } catch (InterruptedException e) { ic ++; System.out.println("InterruptedException observed when isDone() == true " + c + " " + ic + " " + Thread.currentThread()); } } }; CompletableFuture.runAsync(task); Stream.generate(observe::get) .limit(Runtime.getRuntime().availableProcessors() - 1) .forEach(CompletableFuture::runAsync); Thread.sleep(1000); System.exit(0); }
先看一下这段代码的核心逻辑:
首先标号为 ① 的地方是两个计数器,c 表明的是第一个 while 循环的次数,ic 表明的是抛出 InterruptedException(IE) 的次数。
标号为 ② 的地方是判断当前任务是不是完成状态,若是是,则继续往下。
标号为 ③ 的地方是先中断当前线程,而后调用 get 方法获取任务结果。
标号为 ④ 的地方是若是 get 方法抛出了 IE 异常,则在这里进行记录,打印日志。
须要注意的是,若是打印日志了,说明了一个问题:
前面明明 isDone 方法返回 true 了,说明方法执行完成了。可是我调用 get 方法的时候却抛出了 IE 异常?
这你怕是有点说不通吧!
JDK 8 的运行结果我给你们截个图。
这个异常是在哪里被抛出来的呢?
awaitDone 方法的入口处,就先检查了当前线程是否被中断,若是被中断了,那么抛出 IE 异常:
而代码怎么样才能执行到 awaitDone 方法呢?
任务状态是小于等于 COMPLETING 的时候。
在示例代码中,前面的 while 循环中的 isDone 方法已经返回了 true,说明当前状态确定不是 NEW。
那么只剩下个什么东西了?
就只有一个 COMPLETING 状态了。
小样,这不就是监测到了吗?
在这段示例代码出来后的第 8 个小时,David 靓仔又来讲话了:
他要表达的意思,我理解的是这样的:
在 j.u.c 包里面,优先检查线程中断状态是很常见的操做,由于相对来讲,会致使线程中断的地方很是的少。
可是不能由于少,咱们就不检查了。
咱们仍是得对其进行了一个优先检查,告知程序当前线程是否发生了中断,便是否有继续往下执行的意义。
可是,在这个场景中,当前线程中断了,但并不能表示 Future 里面的 task 任务的完成状况。这是两个不相关的事情。
即便当前线程中断了,可是 task 任务仍然能够继续完成。可是执行 get 方法的线程被中断了,因此可能会抛出 InterruptedException。
所以,他给出的解决建议是:
能够选择优先返回结果,在 awaitDone 方法的循环中把检查中断的代码挪到后面去。
五天以后,以前 BUG 的提交者 Martin 同窗又来了:
他说他改变主意了。
改变什么主意了?他以前的主意是什么?
在 Doug 说他是故意这样写的以后,Martin 说:
It's intentional。哦,原来是故意的呀。
那个时候他的主意就是:大佬都说了,这样写是考虑过的,确定没有问题。
如今他的主意是:若是 isDone 方法返回了 true,那么 get 方法应该明确的返回结果值,而不会抛出 IE 异常。
须要注意的是,这个时候对于 BUG 的描述已经发生变化了。
从“FutureTask.isDone 方法在任务尚未完成的时候就会返回 true”变成了“若是 isDone 方法返回了 true,那么 get 方法应该明确的返回结果值,而不会抛出 IE 异常”。
而后 David 靓仔给出了一个最简单的解决方案:
最简单的解决方案就是先检查状态,再检查当前线程是否中断。
而后,这个 BUG 由 Martin 同窗进行了修复:
修复的代码能够先不看,下面一小节我会给你们作个对比。
他修复的同时还当心翼翼的要求 Doug 祝福他,为他站个台。
最后,Martin 同窗说他已经提交给了 jsr166,预计在 JDK 9 版本进行修复。
出于好奇,我在 JDK 的源码中搜索了一下 Martin 同窗的名字,本觉得是个青铜,没想到是个王者,失敬失敬:
既然说在 JDK 9 中对该 BUG 进行了修复,那么带你们对比一下 JDK 9/8 的代码。
java.util.concurrent.FutureTask#awaitDone:
能够看到,JDK 9 把检查是否中断的操做延后了一步。
代码修改成这样后,把以前的那段示例代码放到 JDK 9 上面跑一下,你会惊奇的发现,没有抛出异常了。
由于源码里面判断 COMPLETING 的操做在判断线程中断标识以前:
我想就不须要我再过多解释了吧。
而后多说一句 JDK 9 如今的 FutureTask#awaitDone 里面有这样的一行注释:
它说:isDone 方法已经告诉使用者任务已经完成了,那么调用 get 方法的时候咱们就不该该什么都不返回或者抛出一个 IE 异常。
这行注释想要表达的东西,就是上面一小节的 BUG 里面咱们在讨论的事情。写这行注释的人,就是 Martin 同窗。
当我了解了这个 BUG 的前因后果以后,又忽然间在 JDK 9 的源码里面看到这个注释的时候,有一种很神奇的感受。
就是一种源码说:you feel me?
我立刻心照不宣:I get you。
挺好。
在 JDK 9 的注释里面还有这个词汇:
spurious wakeup,虚假唤醒。
若是你以前不知道这个东西的存在,那么恭喜你,又 get 到了一个你基本上用不到的知识点。
除非你本身须要在代码中用到 wait、notify 这样的方法。
哦,也不对,面试的时候可能会用到。
“虚假唤醒”是怎么一回事呢,我给你看个例子:
java.lang.Thread#join(long) 方法:
这里为何要用 while 循环,而不是直接用 if 呢?
由于循环体内有调用 wait 方法。
为何调用了 wait 方法就必须用 while 循环呢?
别问,问就是防止虚假唤醒。
看一下 wait 方法的 javadoc:
一个线程能在没有被通知、中断或超时的状况下唤醒,也即所谓的“虚假唤醒”,虽然这点在实践中不多发生,可是程序应该循环检测致使线程唤醒的条件,并在条件不知足的状况下继续等待,来防止虚假唤醒。
因此,建议写法是这样的:
在 join 方法中,isAlive 方法就是这里的 condition does not hold。
在《Effective Java》一书中也有提到“虚假唤醒”的地方:
书中的建议是:没有理由在新开发的代码中使用 wait、notify 方法,即便有,也应该是极少了,请多使用并发工具类。
再送你一个面试题:为何 wait 方法必须放在 while 循环体内执行?
如今你能回答的上来这个问题了吧。
关于“虚假唤醒”就说这么多,有兴趣的同窗能够再去仔细了解一下。
好好的说着 JDK 的 FutureTask 呢,怎么忽然转弯到 Netty 上了?
由于 Netty 里面,其核心的 Future 接口实现中,犯了一个基本的逻辑错误,在实现 cancel 和 isDone 方法时违反了 JDK 的约定。
这是一个让 Netty 做者也感到惊讶的错误。
先看看 JDK Future 接口中,对于 cancel 方法的说明:
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html
文档的方法说明上说:若是调用了 cancel 方法,那么再调用 isDone 将永远返回 true。
看一下这个测试代码:
能够看到,在调用了 cancel 方法后,再次调用 isDone 方法,返回的确实 false。
这个点我是好久以前在知乎的这篇文章上看到的,和本文讨论的内容有一点点相关度,我就又翻了出来,多说了一嘴。
有兴趣的能够看看:《一个让Netty做者也感到惊讶的错误》
好啦,才疏学浅,不免会有纰漏,若是你发现了错误的地方,能够在留言区提出来,我对其加以修改。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。