未初始化的对象泄漏到另一个线程,尽管没有代码显式泄漏它



让我们看看这个简单的Java程序:

import java.util.*;
class A {
    static B b;
    static class B {
        int x;
        B(int x) {
            this.x = x;
        }
    }
    public static void main(String[] args) {
        new Thread() {
            void f(B q) {
                int x = q.x;
                if (x != 1) {
                    System.out.println(x);
                    System.exit(1);
                }
            }
            @Override
            public void run() {
                while (b == null);
                while (true) f(b);
            }
        }.start();
        for (int x = 0;;x++)
            b = new B(Math.max(x%2,1));
    }
}
主线程

主线程创建B的实例,将x设置为1,然后将该实例写入静态字段A.b。它会永远重复这个动作。

轮询线程

生成的线程轮询,直到发现A.b.x不是1。

? ! ?

一半的时间像预期的那样进入无限循环,但一半的时间我得到这样的输出:

$ java A
0

为什么轮询线程能够看到B, x没有设置为1?


x%2而不是x在这里只是因为问题是可重复的。


我在linux x64上运行openjdk 6

这是我的想法:因为b不是final,编译器可以自由地重新排序操作,因为它喜欢,对吗?因此,从根本上说,这是一个重新排序问题,因此不安全的发布问题将变量标记为final将解决这个问题。

或多或少,它与Java内存模型文档中提供的示例相同。

真正的问题是这怎么可能。我也可以在这里推测(因为我不知道编译器将如何重新排序),但也许在写x发生之前,对B的引用被写入主内存(对其他线程可见)。在这两个操作之间发生读操作,因此零值

围绕并发性的考虑通常集中在对状态或死锁的错误更改上。但是来自不同线程的状态可见性同样重要。在现代计算机中有许多地方可以缓存状态。在寄存器中,处理器上的L1缓存,处理器和存储器之间的L2缓存,等等。JIT编译器和Java内存模型被设计成尽可能或合法地利用缓存,因为它可以加快速度。

它也可以给出意想不到的和违反直觉的结果。我相信这种情况正在发生。

当创建B的实例时,在将实例变量x设置为传递给构造函数的任何值之前,它被短暂地设置为0。在这种情况下,1。如果另一个线程试图读取x的值,即使x已经被设置为1,它也可以看到值0。它可能看到一个过时的缓存值。

要确保看到x的最新值,您可以做几件事。您可以使x易失性,或者您可以在B实例上通过同步保护x的读(例如,通过添加synchronized getX()方法)。您甚至可以将x从int类型更改为java.util.concurrent.atomic.AtomicInteger类型。

但到目前为止,纠正这个问题最简单的方法是使x为终值。在B的生命周期中它永远不会改变。Java对final字段做了特殊保证,其中之一是在构造函数完成后,构造函数中设置的final字段对任何其他线程都是可见的。也就是说,其他线程不会看到该字段的过期值。

使字段不可变还有许多其他好处,但这是一个很大的好处。

参见Jeremy Manson的《原子性、可见性和排序》。尤其是他说:

(注意:当我在这篇文章中说同步时,我实际上并不是指锁定。我指的是任何在Java中保证可见性或排序的东西。这可以包括final和volatile字段,以及类初始化,线程启动和连接以及各种其他好东西。

在我看来,B.x上可能存在竞争条件,因此可能存在B.x已经创建并且B.x=0的瞬间。在B的构造函数中x = x。事件序列类似于:

B is created (x defaults to 0) -> Constructor is ran -> this.x = x

你的线程在B.x创建之后的某个时间访问B.x,但在构造函数运行之前。但是,我无法在本地重新创建此问题。

最新更新