簡體   English   中英

使此可見性示例失敗

[英]Making this visibility example fail

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中發生了變化?

編輯 :相關問題: 非線程安全對象發布

但我想知道它在實踐中發生的可能性有多大。

非常不可能的esp,因為JIT可以將n轉換為局部變量並且只讀取一個。

極不可能的線程安全漏洞的問題在於,有一天,您可能會更改一些無關緊要的內容,例如您選擇的處理器或JVM,並且突然您的代碼會隨機中斷。

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

不保證您也可以重現它。

有沒有直接的方法來編寫一個測試,證明這個可見性問題不僅僅是理論上的?

在某些情況下,是的。 但是這個是難以證明的,部分原因是因為JVM沒有被阻止比JLS說的最小的線程更安全。

例如,HotSpot JVM通常會執行您期望的操作,而不僅僅是文檔中的最小值。 例如,根據javadoc,System.gc()只是一個提示,但默認情況下,HotSpot JVM每次都會這樣做。

這種情況非常不可能,但仍有可能。 在加載比較中的第一個n之前,線程必須暫停,在第二個比較之前並且它們被比較 - 這需要非常小的一小部分,你必須非常幸運地擊中它。 但是如果你把這個代碼寫在一個超級關鍵的應用程序中,數百萬用戶每天都會在全世界范圍內使用它,它遲早會發生 - 這只是一個時間問題。

無法保證您可以重現它 - 甚至可能在您的機器上無法重現。 這取決於您的平台,VM,Java編譯器等...

您可以將前n轉換為局部變量,然后暫停線程(Sleep),並在進行比較之前讓第二個線程更改n。 但我認為這種情況會破壞證明你案件的目的。

如果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();
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM