2017-05-08 53 views
4

的我的問題是給帖子: https://shipilev.net/blog/2014/safe-public-construction/Java內存模型和重排序操作

public class UnsafeDCLFactory { 
    private Singleton instance; 

    public Singleton get() { 
    if (instance == null) { // read 1, check 1 
     synchronized (this) { 
     if (instance == null) { // read 2, check 2 
      instance = new Singleton(); // store 
     } 
     } 
    } 
    return instance; // read 3 
    } 
} 

而且,它寫的是:在這個代碼實例

請注意,我們做的幾個讀,並且至少「讀取1」和「讀取3」是沒有任何同步的讀取 - 也就是說,這些讀取是活潑的。 Java內存模型 的其中一個意圖是允許重新排序以進行普通讀取,否則性能成本會過高。 在規則方面,正如前面提到的一致性規則一樣, 讀取操作可以觀察通過競爭的無序寫入。這是 決定的每個讀取操作,無論其他操作 已讀取相同的位置。在我們的例子中,這意味着即使 雖然「讀取1」可以讀取非null實例,然後代碼在 上移動以返回它,然後它執行另一個讀取讀取,並且它可以讀取 null實例,這將是回!

我無法理解它。我同意編譯器顯然可以重新排序內存操作。但是,這樣做,編譯器必須從單線程的點視圖保留原始程序的行爲。

在上例中,read 1讀取非空。 read 3閱讀null。這意味着編譯器對read 3進行了重新排序,並且讀取instance的優先級爲read 1(我們可以跳過CPU重新排序,因爲該帖子會引發Java內存模型)。

但是,在我的眼睛read 3不能超越read 1因爲店操作 - 我的意思是instance = new Singleton();

畢竟,有數據依賴關係,這意味着編譯器不能重新排序指令read 3store,因爲它改變的意義該程序(甚至是單線程)。編譯器也不能更改read 1的順序,因爲它必須在store之前。否則,單線程程序的語義是不同的。

因此,順序必須是:read 1 -> store -> read 3

你怎麼看呢?

P.S.發佈什麼意味着什麼?特別是,發佈一些不安全的東西意味着什麼?


這是對@Aleksey Shipilev答案的重新回答。

讓我再說一遍 - 構建示例失敗並不能否定規則。

是的,很明顯。

而Java Memory Model允許在第二次讀取時返回null。

我同意這一點。我不認爲它不允許。 (可能是因爲數據競賽 - 是的,它們是邪惡的)。我聲稱read 3不能超過read 1。我知道你是對的,但我想明白這一點。我仍然聲稱Java編譯器無法生成read 3接管read 1的此類字節碼。我看到read3由於數據競爭可以讀取null,但我無法想象read 1讀取非null和read 3讀取爲空而read 3由於數據依賴性無法超越read 1

(這裏不考慮對硬件(CPU)級的存儲排序)

+0

「編譯器必須從一個線程的角度出發保持原始程序的behaviuor」 - 是什麼讓你說呢? – user2357112

+0

另外,如果讀取1讀取非空值,則此線程**不會執行存儲**。沒有要求保持相對於沒有發生的操作的順序。其他一些線程可能會執行一個存儲,但是如果沒有同步,相對於其他線程存儲的訂購要求相當寬鬆。 – user2357112

+0

「這個線程不執行存儲」,是的,但是當編譯器編譯代碼時,它不知道'instance'是否爲null。 – Gilgamesz

回答

2

但是,這樣做,編譯器必須從單線程的點視圖保留 原始程序的行爲。

沒有。它必須保留語言規範的要求。在這種情況下,JMM。如果某些轉換在JMM下是合法的,則可以執行該轉換。 「單線的觀點」不是規範的語言,規範是。

而Java存儲器模型允許在第二次讀取時返回null。如果你不能構建實際的轉換,那並不意味着這種轉換是不可能的。讓我再說一遍 - 未能構建示例而不是反駁該規則。現在你可以看到的例子轉型"Benign Data Races are Resilient",也看過這一段有:

這似乎違反直覺的:如果我們從實例看空,我們 採取糾正措施與儲存新的實例,那就是它。 事實上,如果我們有中間商店的實例,我們不能看到 的默認值,並且我們只能看到該商店(因爲它 發生在我們之前的所有符合執行中,其中第一次讀取的 返回null)或商店從另一個線程(這不是空的, 只是一個不同的對象)。但是當我們在第一次讀取時 沒有從實例讀取空值時,有趣的行爲展開。沒有中介 商店發生。第二次讀取嘗試再次讀取,並且原樣爲 ,可能會讀取空值。哎喲。

也就是說,您可以輕鬆地轉換程序以顯示沒有干預寫入的路徑,並且有輕微的讀取重新排序會給出「反直覺」結果。字節碼不是程序可以使用的唯一「轉換」。上面的鏈接概述了公開代碼路徑而不依賴於數據依賴關係的編譯器轉換。即這兩個程序是微妙的不同:

// Case 1 
Object o1 = instance; 
instance = new Object(); 
Object o2 = instance; 

// Case 2 
Object o1 = instance; 
if (o1 == null) 
    instance = new Object(); 
Object o2 = instance; 

在「案例二」中,有避免店裏instance的路徑 - 因爲程序現在有兩條路,由於分支 - 優化轉換可以暴露它。

這是一個可能出錯的人爲的例子。在真正的程序中,控制和數據流更加複雜,優化轉換允許(並且最終)產生相似的結果,因爲它們運行在一組派生規則上,如「不同步,無數據依賴性 - 免費移動東西「,經過一小段顯示有趣行爲的轉換。

數據競賽是邪惡的。

+0

感謝您的關注。我編輯了我的帖子,並在回答中附加了問題。我在評論中沒有要求你說清楚。 – Gilgamesz

+0

您是否閱讀過我發佈的鏈接?我在修改後的答案中總結了它。 –

+0

哦,是的,我沒有'注意到你給了一個鏈接。我會讀它,謝謝:) – Gilgamesz

0

考慮一類

class Foo { String bar = "qux"; } 

當讀取Foo::bar,你總是希望它返回字符串"qux"。然而構造函數調用不是原子的。它可以寧願被分成兩個部分:

Foo foo = <init> Foo; 
foo.bar = "qux"; 

不安全的出版物,一個線程可能會從它的本地存儲器出版Foo但不是它的價值bar。當讀取一個volatile字段需要所有內存的同步時,這個問題通過上述模式解決。

1

有一個簡單的方法來回避這個最:

public enum SafeSingleton { 
    INSTANCE; 
} 
+0

@Gilgamesz,Andreas是正確的,'實例== null'檢查不是線程安全的。枚舉Singleton是線程安全的,並且可以像你一樣簡單。 – Novaterata