Java併發筆記1-底層實現volatile、synchronized、原子操作
volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的“可見性”。可見性:當一個執行緒修改一個變數時,另外一個執行緒能讀到這個修改的值。如果volatile變數修飾符使用恰當的話,他比synchronized的使用和執行成本更低,因為他不會引起執行緒上下文的切換和排程。
原子操作:不可中斷的一個或一系列操作
Lock字首指令在多核處理器會引發了兩件事。
1)將當前處理器快取行的資料寫回到系統記憶體
2)這個寫記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效。
volatile:如果對聲明瞭volatile的變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。
volatile的兩條實現原則:
1)Lock字首指令會引起處理器快取寫回到記憶體。
2)一個處理器的快取寫到記憶體會導致其他處理器的快取無效。
synchronized實現同步的基礎:Java中的每一個物件都可以作為鎖。具體表現為以下3種形式:
對於普通同步方法,鎖是當前例項物件
對於靜態同步方法,鎖是當前類的物件
對於同步程式碼塊,鎖是synchronized括號裡配置的物件
當一個執行緒試圖訪問同步程式碼塊時,它必須得到鎖,退出或丟擲異常時必須釋放鎖。
JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步。程式碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是另外一種方式實現的。
monitoreter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorter必須有對應的monitorexit與之配對。任何物件都有一個monitor與之關聯,當且一個monitor被持有後,他講處於鎖定狀態。執行緒執行到monitorenter指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖。
JavaSE1.6中鎖一共有4種狀態:無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態。
- 偏向鎖:1.當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存偏向鎖的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下MarkWord中偏向鎖的標識是否設定成為1(表示當前是偏向鎖);如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。2.偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有正在執行的位元組碼)。首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著,如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態;如果執行緒仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的MarkWord要麼偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖,最後喚醒暫停的執行緒。3.關閉偏向鎖:在Java6和7是預設啟動,但是在程式啟動幾秒後在啟用。可以使用JVM引數關閉延遲。如果你確定應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖。
- 輕量級鎖:1.輕量級鎖加鎖:執行緒在執行同步塊之前,JVM會先在當前執行緒的棧幀中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,。然後執行緒嘗試使用CAS將物件頭中的MarkWord替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲得鎖。2.解鎖:輕量級解鎖時,會使用原子的CAS操作將DIsplaced MarkWord替換回到物件頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。3.因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了),一旦鎖升級為重量級鎖,就不會再恢復到輕量級鎖狀態,其他執行緒試圖獲取鎖時,都會被阻塞住,當持有鎖的執行緒被釋放之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭。
- 重量級鎖
-
3原子操作的實現鎖 優點 缺點 適用場景 偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果執行緒間存在競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個執行緒訪問同步塊場景 輕量級鎖 競爭的執行緒不會阻塞,提高了程式的響應速度 如果始終得不到鎖競爭的執行緒,使用自旋會消耗CPU。 追求響應時間同步塊執行速度非常快。 重量級鎖 執行緒競爭不使用自旋,不會消耗CPU 執行緒阻塞,響應時間緩慢 追求吞吐量同步塊執行速度較長。 - 原子操作:不可中斷的一個或一系列操作
- 1)使用匯流排鎖保證原子性:第一個機制是通過匯流排鎖保證原子性。所謂匯流排鎖就是使用處理器提供的一個Lock訊號,當一個處理器在總線上輸出此訊號時,其他處理器將被阻塞住,那麼該處理器可以獨佔共享記憶體。
- 2)使用快取鎖保證原子性:第二個機制是通過快取來保證原子性。所謂“快取鎖定”是指記憶體區域如果被快取在處理器的快取行中,並且在Lock操作期間被鎖定,那麼當他執行鎖操作回寫到記憶體時,處理器不在總線上聲言LOCK#訊號,而是修改內部的記憶體地址,並允許他的快取一致性機制來保證操作的原子性,因為快取一致性機制會阻止同時修改由兩個以上處理器快取的記憶體區域資料,當其他處理器回寫已被鎖定的快取行的資料時,會使快取行無效。但是有兩種情況下處理器不會使用快取鎖定:1擋操作的資料不能被快取在處理器內部,或操作的資料跨多個快取行(cache line),則處理器回撥用匯流排鎖定。2有些處理器不支援快取鎖定
- 3)Java實現原子操作
- 在Java中可以通過鎖和迴圈CAS實現原子操作。使用迴圈CAS實現原子操作
- JDK的併發包提供了一些類支援原子操作如:AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新int值)、AtomicLong(用原子方式更新long值)。這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當前值自增1或自減1.
- 實現原子操作的三大問題:1)ABA問題:解決思路-加上版本號:Atomic包裡compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。2)迴圈時間長開銷大:JVM能支援處理器提供的pause指令,那麼效率會有一定提升。3)只能保證一個共享變數的原子操作: 對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖。把多個共享變數合併成一個共享變數來操作。
- 使用鎖機制實現原子操作:
- 鎖機制保證了只有獲得鎖的執行緒才能操作鎖的記憶體區域。JVM內部實現了很多種鎖機制,有偏向鎖,輕量級鎖和互斥鎖。除了偏向鎖,JVM實現鎖的方式都用了迴圈CAS,即當一個執行緒想進入同步塊的時候使用迴圈CAS的方式來獲取鎖,當它退出同步塊的時候使用迴圈CAS釋放鎖。