簡體   English   中英

沒有 volatile 的雙重檢查鎖是錯誤的?

[英]double check lock without volatile is wrong?

我使用 jdk1.8。 我認為沒有 volatile 的雙重檢查鎖是正確的。 我多次使用 countdownlatch 測試,對象是單例。 如何證明它一定需要“volatile”?

更新 1

抱歉,我的代碼沒有格式化,因為我無法接收一些 JavaScript 公共類 DCLTest {

private static /*volatile*/ Singleton instance = null;

static class Singleton {

    public String name;

    public Singleton(String name) {
        try {
            //We can delete this sentence, just to simulate various situations
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.name = name;
    }
}

public static Singleton getInstance() {
    if (null == instance) {
        synchronized (Singleton.class) {
            if (null == instance) {
                instance = new Singleton(Thread.currentThread().getName());
            }
        }
    }
    return instance;
}

public static void test() throws InterruptedException {
    int count = 1;
    while (true){
        int size = 5000;
        final String[] strs = new String[size];
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < size; i++) {
            final int index = i;
            new Thread(()->{
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Singleton instance = getInstance();
                strs[index] = instance.name;
            }).start();
        }
        Thread.sleep(100);
        countDownLatch.countDown();
        Thread.sleep(1000);
        for (int i = 0; i < size-1; i++) {
            if(!(strs[i].equals(strs[i+1]))){
                System.out.println("i = " + strs[i] + ",i+1 = "+strs[i+1]);
                System.out.println("need volatile");
                return;
            }
        }
        System.out.println(count++ + " times");
    }
}

public static void main(String[] args) throws InterruptedException {
    test();
}

}

您沒有看到的關鍵問題是指令可以重新排序。 因此,它們在源代碼中的順序與它們在內存中的應用順序不同。 CPU 和編譯器是原因或這種重新排序。

我不會詳細介紹雙重檢查鎖定示例的整個示例,因為有很多示例可用,但將為您提供足夠的信息來進行更多研究。

如果您有以下代碼:

 if(singleton == null){
     synchronized{
         if(singleton == null){
            singleton = new Singleton("foobar")
         }
     }
 }

然后在引擎蓋下會發生這樣的事情。

if(singleton == null){
     synchronized{
         if(singleton == null){
            tmp = alloc(Singleton.class)
            tmp.value = "foobar"
            singleton = tmp
         }
     }
 }

到目前為止,一切都很好。 但以下重新排序是合法的:

if(singleton == null){
     synchronized{
         if(singleton == null){
            tmp = alloc(Singleton.class)
            singleton = tmp
            tmp.value = "foobar"
         }
     }
 }

所以這意味着尚未完全構造的單例(尚未設置值)已寫入單例全局變量。 如果一個不同的線程讀取這個變量,它可以看到一個部分創建的對象。

還有其他潛在的問題,如原子性(例如,如果值字段很長,它可能會被碎片化,例如讀/寫撕裂)。 還有可見性; 例如,編譯器可以優化代碼,以便優化內存中的加載/存儲。 請記住,從內存而不是緩存中讀取的思考從根本上是有缺陷的,也是我在 SO 上看到的最常遇到的誤解; 甚至很多前輩都弄錯了。 原子性、可見性和重新排序是 Java 內存模型的一部分,並且使單例變量可變,解決了所有這些問題。 它消除了數據競爭(您可以查找它以獲取更多詳細信息)。

如果你想成為真正的核心,在創建對象和分配給單例之間放置一個 [storestore] 屏障就足夠了,在讀取端放置一個 [loadload] 屏障就足夠了。 但這遠遠超出了大多數工程師的理解,並且在大多數情況下不會對性能產生太大影響。

如果您想檢查某些東西是否會損壞,請查看 JCStress:

https://github.com/openjdk/jcstress

它是一個很棒的工具,可以幫助您證明您的代碼已損壞。

如何證明它一定需要“volatile”?

作為一般規則,您無法通過測試來證明多線程應用程序的正確性。 也許能夠證明不正確,但即使如此也不能保證。 正如你在觀察。

您沒有成功地使您的應用程序失敗的事實並不能證明它是正確的。

證明正確性的方法是分析之前進行形式化(即數學)。

可以很直接地表明,當singleton不是volatile ,在執行之前會發生缺失。 可能會導致不正確的結果,例如多次進行初始化。 但不能保證您會得到不正確的結果。

另一方面是,如果使用volatile ,則發生在關系與代碼邏輯相結合之前發生的事情足以構建正式(數學)證明,您將始終得到正確的結果。


(我不打算在這里構造證明。太費力了。)

暫無
暫無

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

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