关于后端:Doug-Lea在JUC包里面写的BUG又被网友发现了

3次阅读

共计 7935 个字符,预计需要花费 20 分钟才能阅读完成。

这是 why 的第 69 篇原创文章

BUG 形容

一个编号为 8073704 的 JDK BUG,将串联起我的这篇文章。

也就是上面的这个链接。

https://bugs.openjdk.java.net/browse/JDK-8073704

这个 BUG 在 JDK 9 版本中进行了修复。也就是说,如果你用的 JDK 8,兴许会遇到这样的问题。

先带大家看看这个问题是怎么样的:

这个 BUG 说:FutureTask.isDone 办法在工作还没有实现的时候就会返回 true。

能够看到,这是一个 P4 级别(优先级不高)的 BUG,这个 BUG 也是调配给了 Doug Lea,因为 FutureTask 类就是他写的:

响应了国家政策:谁净化,谁治理。

这个 BUG 的作者 Martin 老哥是这样形容的:

上面我会给大家翻译一下他要表白的货色。

然而在翻译之前,我得先做好背景铺垫,免得有的敌人看了后一脸懵逼。

如果要懂他在说什么,那我必须得再给你看个图片,这是 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 的小老弟接过了话筒,他这样说的:

我感觉吧,保罗说的挺有情理的,我赞成他的倡议。

然而吧,我也感觉咱们在探讨的是一个十分细节,十分小的问题,我不晓得,就算当初这样写,会导致任何问题吗?

写到这里,先给大家捋一下:

  • Martin 老哥提出 BUG 说 FutureTask#isDone 办法没有判断 INTERRUPING 和 COMPLETING 这个两个中间状态是不对的。
  • Paul 同学说,对于 INTERRUPING 这个状态,能够参照 isCancelled 办法,不须要做非凡判断。
  • Chris 小老弟说 Paul 同学说的对。

于是他们感觉 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 循环体内执行?

当初你能答复的上来这个问题了吧。

对于“虚伪唤醒”就说这么多,有趣味的同学能够再去认真理解一下。

Netty 的一个坑

好好的说着 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 作者也感到诧异的谬误》

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

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

正文完
 0