我有這樣一段代碼,它表現出一個執行緒對快取的陳舊讀取,這使得它無法退出其while回圈。
class MyRunnable implements Runnable {
boolean keepGoing = true; // volatile fixes visibility
@Override public void run() {
while ( keepGoing ) {
// synchronized (this) { } //修復可見性
// Thread.yield(); // 修復了可見性。
System.out.println(); //修復了可見性。
}
}
}
class Example {
public static void main title function_">main(String[] args) throws InterruptedException{
MyRunnable myRunnable = new MyRunnable() 。
new Thread(myRunnable).start();
Thread.sleep(100)。
myRunnable.keepGoing = false。
}
我相信Java記憶體模型保證了對易失性變數的所有寫操作與任何執行緒的所有后續讀操作同步,因此這就解決了問題。
如果我的理解是正確的,由同步塊生成的代碼也會沖掉所有待定的讀寫,這就成為了一種 "記憶體屏障",并解決了這個問題。
從實踐中我看到,插入yield和println也會使變數的變化對執行緒可見,并正確退出。我的問題是:
yield/println/io作為記憶體屏障,是由JMM以某種方式保證的,還是一個不能保證作業的幸運副作用?
uj5u.com熱心網友回復:
不,無論是JLS還是你在那里使用的類或方法的javadocs,都沒有保證。
在當前的實作中,在yield()和println中存在實際記憶體障礙。 (如果你深入挖掘實作代碼,你應該能夠弄清楚它們是如何產生的,以及它們的目的是什么。)
然而,我們無法保證這些記憶體障礙將在所有平臺上的所有 Java1實作中存在。 規范沒有規定發生在關系存在之前2,因此它們不要求插入3記憶體屏障。
假想:
假設
Thread.yield()被實作為no-op。 (就像System.gc()可以是一個no-op一樣。)假設輸出流堆疊被優化了,那么它的同步就不再需要在罩子里了。 例如,假設JVM可以推斷出某個特定的輸出流是受執行緒限制的,并且在向其緩沖區寫入時不需要記憶體屏障。
假設
現在,我個人認為這些變化不可能發生。 (而且它們甚至可能不可行。)但是如果它們真的發生了,那么目前依賴于這些偶然的記憶體屏障的許多 "壞 "應用程式很可能會停止作業。
重點是:如果你想要保證,請依靠規格說明。 規范是唯一真正的保證......如果你的代碼需要被移植的話。
1
1 - 特別是,未來的。
2 - 事實上,正如Holger的回答所解釋的, uj5u.com熱心網友回復: 假設我有這樣一段代碼,它顯示了一個執行緒對快取的陳舊讀取,這使得它無法退出其while回圈。
如果你指的是 CPU 快取,那么這就是一個糟糕的心理模型(除了不適合 JMM 的心理模型之外)。現代 CPU 上的快取始終是連貫的。
這一點是正確的。在對易失性變數的寫入和對同一易失性變數的所有后續讀取之間存在著一個發生前的邊緣。
blockquote 如果我的理解是正確的,由同步塊生成的代碼也會沖掉所有待定的讀寫,這就像一種 "記憶體屏障",并解決了這個問題。
結合 JMM 從記憶體屏障的角度進行推理是很危險的。
https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane 在監視器的釋放和該監視器的任何后續獲取之間存在一個發生前的邊緣。因此,如果你在訪問 檢查JLS,你會發現在2個產量之間的邊緣之前沒有發生。也許這涉及到CPU記憶體障礙,但問題可能發生在代碼進入CPU之前。例如,JIT可能會將代碼優化為: 因此在這種情況下,代碼在CPU上執行之前就已經被 "破壞 "了,因為代碼永遠不會看到 "keepGoing "變數的更新版本。
我不確定 Thread.yield() 是否有任何編譯器障礙,如果有編譯器障礙,那么 JIT 就無法優化出負載或存盤。但這些都不是規范的一部分。 uj5u.com熱心網友回復: 規范中沒有任何內容保證任何形式的重繪 。這僅僅是一個錯誤的心理模型,它假設必須有一個類似于主記憶體的東西來維持全域狀態。但是一個執行環境可能在每個CPU上都有本地記憶體,而根本沒有主記憶體。因此,CPU 1向CPU 2發送更新的資料并不意味著CPU 3知道它。 在實踐中,系統有一個主記憶體,但快取可能會被同步,而不需要將資料傳輸到主記憶體。 此外,討論記憶體傳輸最終會陷入一個隧道的視野。Java的記憶體模型還決定了JVM可以執行哪些優化,哪些不可以。例如:
在這里,編譯器有權洗掉條件,并無條件地執行該塊,因為前面的陳述句(忽略睡眠)已經寫了 因此,當這個優化被執行后,有多少執行緒向這個變數寫入新值并 "重繪 到記憶體 "并不重要。這段代碼不會注意到。
因此,讓我們查閱規范 我認為,你的問題的答案再清楚不過了。
為了完整起見, 我相信,Java 記憶體模型保證了對易失性變數的所有寫入都與任何執行緒的所有后續讀取同步,所以這就解決了這個問題。
在寫到 離開
標籤:Thread的javadocs明確指出,你cannot假設或依賴任何同步行為發生在yield()。 這顯然意味著在yield()和任何其他執行緒上的任何動作之間沒有happens before的存在。
3 - 事實上,記憶體障礙是一個實作細節。 它們被一個典型的編譯器用來實作JMM的可見性保證。 關鍵是這些保證,而不是用來實作它們的策略。 因此,當你試圖確定多執行緒代碼是否正確時,任何關于記憶體屏障、快取、暫存器等的討論都是無關緊要的。
我相信,Java 記憶體模型保證了對易失性變數的所有寫入與來自任何執行緒的所有后續讀取同步,因此這就解決了問題。
keepGoing變數時,它受到鎖的保護,就不會出現資料競賽。
yield/println/io是否作為JMM以某種方式保證的記憶體屏障,或者它是一個不能保證作業的幸運副作用?
if(!keepGoing){
return;
}
while(true){
Thread.yield()。
println()。
nonVolatileVar = null。
Thread.sleep(100_000)。
if(nonVolatileVar == null) {
//做一些事情。
null,其他執行緒的活動與非易失性變數無關,不管過了多少時間。需要注意的是,
Thread.sleep和Thread.yield都沒有任何同步語意。特別是,在呼叫Thread.sleep或Thread.yield之前,編譯器不必將快取在暫存器中的寫入物沖到共享記憶體中,也不必在呼叫Thread.sleep或Thread.yield之后重新加載快取的值。
volatile變數之前所做的所有寫操作對于隨后讀取同一變數的執行緒是可見的。所以在你的案例中,將keepGoing宣告為volatile將解決這個問題,因為兩個執行緒都持續使用它。
如果我的理解是正確的,同步塊所產生的代碼也會沖掉所有待定的讀和寫,這可以作為一種 "記憶體屏障 "并修復這個問題。
synchronized塊的執行緒與進入synchronized塊的執行緒建立了happens-before關系,該執行緒使用同一物件。如果在一個執行緒中使用synchronized塊似乎解決了問題,盡管你沒有在另一個執行緒中使用synchronized塊,你正在依賴一個特定實作的副作用,而這并不能保證繼續作業。
