9

Java 并发实践中,我认为让人们(至少是我)感到惊讶的示例之一是这样的:

public class Foo {

  private int n;

  public Foo(int n) {
    this.n = n;
  }

  public void check() {
    if (n != n) throw new AssertionError("huh?");

  }
}

令人惊讶(至少对我来说)是声称这不是线程安全的,不仅不安全,而且检查方法有可能抛出断言错误。

解释是,如果没有同步/将 n 标记为 volatile,则不同线程之间没有可见性保证,并且 n 的值可以在线程读取它时发生变化。

但我想知道它在实践中发生的可能性有多大。或者更好,如果我能以某种方式复制它。

所以我试图编写将触发该断言错误的代码,但没有运气。

是否有一种直接的方法来编写一个测试来证明这个可见性问题不仅仅是理论上的?

还是在最近的 JVM 中发生了变化?

编辑:相关问题:不是线程安全的对象发布

4

3 回答 3

6

但我想知道它在实践中发生的可能性有多大。

esp 极不可能,因为 JIT 可以n变成一个局部变量并且只读取一个。

极不可能的线程安全错误的问题在于,有一天,您可能会更改一些无关紧要的东西,例如您选择的处理器或 JVM,然后您的代码突然随机中断。

或者更好,如果我能以某种方式复制它。

也不保证您可以复制它。

有没有一种直接的方法来编写一个测试来证明这个可见性问题不仅仅是理论上的?

在某些情况下,是的。但这一点很难证明,部分原因是 JVM 没有被阻止比 JLS 所说的最低线程安全。

例如,HotSpot JVM 经常执行您可能期望的操作,而不仅仅是文档中的最低要求。例如 System.gc() 只是根据 javadoc 的提示,但默认情况下 HotSpot JVM 每次都会这样做。

于 2013-06-07T22:00:46.753 回答
3

这种情况非常不可能,但仍有可能。线程必须n在比较中的第一个被加载的那一刻暂停,在第二个被比较之前,这需要一秒钟的时间,以至于你必须非常幸运才能击中它. 但是,如果您在全球数百万用户每天都将使用的超级关键应用程序中编写此代码,那么它迟早会发生——这只是时间问题。

无法保证您可以复制它 - 也许在您的机器上甚至不可能。这取决于您的平台、VM、Java 编译器等...

您可以将第一个n转换为局部变量,然后暂停线程(睡眠),并在进行比较之前让第二个线程更改 n。但我认为这种情况会破坏展示您的案例的目的。

于 2013-06-07T22:06:27.123 回答
0

如果 a不安全地发布,理论上另一个线程在读取两次Foo时可以观察到两个不同的值。n

由于这个原因,以下程序可能会失败。

public static Foo shared;

public static void main(String[] args)
{
    new Thread(){
        @Override
        public void run()
        {
            while(true)
            {
                Foo foo = shared;
                if(foo!=null)
                    foo.check();
            }
        }
    }.start();

    new Thread(){
        @Override
        public void run()
        {
            while(true)
            {
                shared = new Foo(1);  // unsafe publication
            }
        }
    }.start();
}

然而,几乎不可能观察到它失败了。VM 可能会优化n!=nfalse没有实际读取n两次。

但是我们可以展示一个等价的程序,即就 Java 内存模型而言,对先前程序的有效转换,并观察它立即失败

static public class Foo
{
    int n;

    public Foo()
    {
    }

    public void check()
    {
        int n1 = n;
        no_op();
        int n2 = n;
        if (n1 != n2)
            throw new AssertionError("huh?");
    }
}

// calling this method has no effect on memory semantics
static void no_op()
{
    if(Math.sin(1)>1) System.out.println("never");
}

public static Foo shared;

public static void main(String[] args)
{
    new Thread(){
        @Override
        public void run()
        {
            while(true)
            {
                Foo foo = shared;
                if(foo!=null)
                    foo.check();
            }
        }
    }.start();

    new Thread(){
        @Override
        public void run()
        {
            while(true)
            {
                // a valid transformation of `shared=new Foo(1)`
                Foo foo = new Foo();
                shared = foo;
                no_op();
                foo.n = 1;
            }
        }
    }.start();
}
于 2013-06-07T23:48:20.567 回答