使此可见性示例失败

时间:2013-06-07 21:54:13

标签: java concurrency

Java Concurrency in Practice其中一个例子中,我认为人们(至少是我)的惊喜是这样的:

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中发生了哪些变化?

编辑:相关问题:Not thread safe Object publishing

3 个答案:

答案 0 :(得分:6)

  

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

非常不可能的esp,因为JIT可以将n转换为局部变量,只读一个。

极不可能的线程安全漏洞的问题是,有一天,您可能会更改一些无关紧要的内容,例如您选择的处理器或JVM,以及您的代码突然随机中断。

  

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

无法保证您可以重现它。

  

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

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

例如,HotSpot JVM通常会按照您的预期执行,而不仅仅是文档中的最小值。例如根据javadoc,System.gc()只是一个提示,但默认情况下,HotSpot JVM每次都会这样做。

答案 1 :(得分:3)

这种情况非常不可能,但仍有可能。在加载比较中的第一个n之前,线程必须暂停,然后在第二个比较之前和它们进行比较 - 这需要你需要的一小部分时间。超级幸运地打了它。但是如果你把这个代码写在一个超级关键的应用程序中,数百万用户每天都会在全世界范围内使用它,它迟早会发生 - 这只是一个时间问题。

无法保证您可以重现它 - 甚至可能在您的机器上无法重现。这取决于您的平台,VM,Java编译器等......

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

答案 2 :(得分:0)

如果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!=n优化为false而不会实际读取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();
}