摘要:有可能一個線程中的動作相對于另一個線程出現亂序。當實際輸出取決于線程交錯的結果時,這種情況被稱為競爭條件。這里的問題在于代碼塊不是原子性的,而且實例的變化對別的線程不可見。這種不能同時在多個線程上執行的部分被稱為關鍵部分。
為什么要額外寫一篇文章來研究volatile呢?是因為這可能是并發中最令人困惑以及最被誤解的結構。我看過不少解釋volatile的博客,但是大多數要么不完整,要么難以理解。我會從并發中最重要的一些因素開始說起:
原子性
原子性是不可分割的操作。它們要么全部實現,要么全部不實現。Java中原子操作的最佳例子是將一個值賦給變量。
可見性
可見性是指:無論是哪個線程對一個共享的變量作出的修改或是帶來的影響,讀其他的線程都是可見的。
有序性
有序性是指源碼中指令是否會被編譯器出于優化而改變執行順序。有可能一個線程中的動作相對于另一個線程出現亂序。
現在舉一個例子來理解這些因素:
public class MyApp { private int count = 0; public void upateVisitors() { ++count; //increment the visitors count } }
Hint: read-modify-write
這一段代碼中有一個試圖更新應用(網頁)的訪客數量的方法。這段代碼的問題在于++count指令不是原子性的,它包含三條獨立的指令:
temp = count; (read) temp = temp + 1; (modify) count = temp; (write)
因此,當一個線程正在執行此操作時,此指令可以被另一個線程預占。從而不是原子性操作。假設count的值為10,并且有如下的執行順序:
我們會發現:在某個很不巧合的時刻,兩個線程同時讀取到了值(10),然后彼此將其值加一。所以在這個過程有一個遞增的操作丟失了。當實際輸出取決于線程交錯的結果時,這種情況被稱為競爭條件(race condition)。這里丟失了一次遞增。那么并發的哪些方面在這里缺失了?原子性。再考慮一個創建單例的例子(當然也是不好的例子):
public Singleton getInstance() { if(_instance == null) { _instance = new Singleton(); } }
Hint: check-then-act
再一次的,可能有兩個線程都判斷這實例為null,并且都進入了if代碼塊。這會導致兩個實例的創建。這里的問題在于代碼塊不是原子性的,而且實例的變化對別的線程不可見。這種不能同時在多個線程上執行的部分被稱為關鍵部分(critical section)。對于關鍵部分,我們需要使用synchronized塊和synchronized方法。
還是原子性
為了確保原子性,我們通常使用鎖來確保互斥。參考下面的例子,一個銀行賬戶使用synchronized方法上鎖。
class BankAccount { private int accountBalance; synchronized int getAccountBalance() { return accountBalance; } synchronized void setAccountBalance(int b) throws IllegalStateException { accountBalance = b; if (accountBalance < 0) { throw new IllegalStateException("Sorry but account has negative Balance"); } } void depositMoney(int amount) { int balance = getAccountBalance(); setAccountBalance(balance + amount); } void withdrawMoney(int amount) { int balance = getAccountBalance(); setAccountBalance(balance - amount); } }
對共享變量balance的訪問通過鎖來保護,從而數據競爭不會有問題。這個類有問題嗎?是有的。假設一個線程調用depositMoney(50)而另一個線程調用withdrawMoney(50),并且balance的初始值為100。理想情況下操作完成后balance應該為0。但是我們無法保證得到這個結果:
depositMoney操作讀取的balance值為100
withdrawMoney操作讀取的balance值也是100,它在此基礎上減去50元并將其設為50元。
最終depositMoney在之前看到的balance值的基礎上加上50,并將其設為150。
再次因為沒有保證原子性而丟失了一個更新。如果兩種方法都被聲明為同步,則將在整個方法期間確保鎖定,并且改變將以原子方式進行。
再談可見性
如果一個線程的操作對另一個線程可見,那么其他線程也會觀察到它的所有操作的結果。考慮下面的例子:
public class LooperThread extends Thread { private boolean isDone = false; public void run() { while( !isDone ) { doSomeWork(); } } public void stopWork() { isDone = true; } }
這里缺失了什么?假設LooperThread的一個實例正在運行,主線程調用了stopWord來中止它。這兩個線程之間沒有實現同步。編譯器會以為在第一個線程中沒有對isDone執行寫入操作,并且決定只讀入isDone一次。于是,線程炸了!部分JVM可能會這樣做,從而使其變成無限循環。因此答案顯然是缺乏可見性。
再談有序性
有序性是關于事情發生的順序。考慮下面的例子:
在上述情況下,線程2能打印出value = 0嗎?其實是有可能的。在編譯器重新排序中result=true可能會在value=1之前出現。value = 1也可能不對線程2可見,然后線程2將加載value = 0。我們可以使用volatile解決這個問題嗎?
CPU架構(多層RAMs)
CPU現在通常多核,并且線程將在不同核心上運行。另外還有不同級別的高速緩存,如下圖所示:
當一個volatile變量被任何線程寫入一個特定的核心,所有其他核心的值都需要更新,因為每個核心都有其自己的緩存,該緩存內有變量的舊值。消息傳遞給所有內核以更新值。
volatile
根據Java文檔,如果一個變量被聲明為volatile,那么Java內存模型(在JDK 5之后)確保所有線程都看到變量的一致值。volatile就像是synchronized的一個親戚,讀取volatile數據就像是進入一個synchronized塊,而寫入volatile數據就像是從synchronized塊中離開。當寫入一個volatile值時,這個值直接寫入主存而不是本地處理器的緩存,并且通過發送消息提醒其它內核的緩存該值的更新。Volatile不是原子性操作
volatile保證順序性和可見性但是不保證互斥或是原子性。鎖能保證原子性,可視性和順序性。所以volatile不能代替synchronized。
volatile讀與寫
volatile提供了順序性保障,這意味著編譯器生成的指令不能以實際源代碼指令定義的順序以外的其他順序執行操作結果。盡管生成的指令的順序可能與源代碼的原始順序不同,但所產生的效果必須相同。我們還需要從Java Doc中觀察以下關于讀寫的內容:
當一個線程讀取一個volatile變量時,它不僅會看到volatile的最新變化,還會看到導致變化的代碼的副作用。
我們需要了解以下有關讀寫volatile的內容:
當一個線程寫入一個volatile變量,另一個線程看到寫入,第一個線程會告訴第二個線程關于內存變化的內容,直到它執行寫入該volatile變量。
在這里,線程2看到了線程1的內容。
我們可以聲明 final 類型的volatile變量嗎?
如果一個變量是final的,我們不能改變它的值,volatile就是確保對其他線程可見的共享變量的更改。所以這是不允許的,并會導致編譯錯誤。
為什么我們在并發編程中聲明long / double為volatile?
默認情況下long/double的讀寫不是原子性的。非原子性的double/long寫操作會被當做兩個寫入操作:分別寫入前32位和后32位。它可能會導致一個線程看到另一個線程寫入的64位值的前32位,而第二個線程看到來自另一個線程寫入的后32位。讀寫volatile的long/double類型變量總是原子性的。
Volatile vs Atomic類
public class MyApp { private volatile int count = 0; public void upateVisitors() { ++count; //increment the visitors count } }
如果我們將count聲明為atomic,這段代碼可以正常運行嗎?可以的,而且當對變量進行增加或減少操作時,最好使用atomic類。AtomicInteger通常使用volatile或是CAS來實現線程安全。
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注我的微信公眾號!將會不定期的發放福利哦~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68804.html
摘要:簡介從創建以來,就支持核心的并發概念如線程和鎖。這篇文章會幫助從事多線程編程的開發人員理解核心的并發概念以及如何使用它們。請求操作系統互斥,并讓操作系統調度程序處理線程停放和喚醒。 簡介 從創建以來,JAVA就支持核心的并發概念如線程和鎖。這篇文章會幫助從事多線程編程的JAVA開發人員理解核心的并發概念以及如何使用它們。 (博主將在其中加上自己的理解以及自己想出的例子作為補充) 概念 ...
摘要:否則它就會用新的值替代當前值。在這種情況下,鎖可能會優于原子變量,但在實際的爭用級別中,原子變量的性能優于鎖。在中引入了另外一個構件。 題目要求 在我們深入了解CAS(Compare And Swap)策略以及它是如何在AtomicInteger這樣的原子構造器中使用的,首先來看一下這段代碼: public class MyApp { private volatile int ...
摘要:內存模型是圍繞著在并發過程中如何處理原子性可見性和有序性這個特征來建立的,我們來看下哪些操作實現了這個特性。可見性可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。 Java內存模型是圍繞著在并發過程中如何處理原子性、可見性和有序性這3個特征來建立的,我們來看下哪些操作實現了這3個特性。 原子性(atomicity): 由Java內存模型來直接保證原子性變量操作包括...
摘要:前半句是指線程內表現為串行的語義,后半句是指指令重排序現象和工作內存和主內存同步延遲現象。關于內存模型的講解請參考死磕同步系列之。目前國內市面上的關于內存屏障的講解基本不會超過這三篇文章,包括相關書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile...
摘要:前半句是指線程內表現為串行的語義,后半句是指指令重排序現象和工作內存和主內存同步延遲現象。關于內存模型的講解請參考死磕同步系列之。目前國內市面上的關于內存屏障的講解基本不會超過這三篇文章,包括相關書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile...
閱讀 1213·2021-11-25 09:43
閱讀 1969·2021-11-11 10:58
閱讀 1187·2021-11-08 13:18
閱讀 2659·2019-08-29 16:25
閱讀 3509·2019-08-29 12:51
閱讀 3307·2019-08-29 12:30
閱讀 748·2019-08-26 13:24
閱讀 3683·2019-08-26 10:38