Java併發 --- 原子變數
Java 併發 --原子變數
原子變量出現的原因 --併發
鎖的劣勢
-
Sysnred
當多個執行緒爭用鎖時,JVM 會藉助作業系統的功能掛起執行緒,等待之後再被恢復(如果用了自旋鎖就不一定會被掛起),這裡的主要開銷就來自於執行緒的掛起和恢復。除此之外,鎖還有可能會帶來類似死鎖、活躍性等問題。 -
Voliet 劣勢
與鎖相比,volatile 變數的使用不會發生上下文切換和執行緒排程,因此它是輕量級的。它提供了「可見性」,但不能用於構造原子的複合操作,繼而也就不能實現計數器或互斥體等工具(i++ 這種自增操作在沒有原子變數之前,只能通過加鎖來保證資料一致性)
我們希望既可以有類似 volatile 的機制,操作是輕量級的,又可以支援原子操作,這就要藉助現代處理器中的一些機制。
- 原子是一個最小的變數
- 原子在java Jdk 中存在的位置
- 在java.util.concurrent.atomic 中,裡面包含了常見的原子型別
CAS(Compare-and-Swap)
CAS 操作在修改目標變數 V 的時候,它先測試 V 的當前值和我們的預期值是否相同,如果相同就設定 V 為新的值。
它的最經典的使用模式就是:
-
從變數 V 中讀取值 A
-
根據值 A 計算得到新的值 B
-
通過 CAS 操作,比較變數 V 當前的值是否還是 A:如果是,就將 V 設定為 B;如果不是,操作失敗,一般不做任何額外操作,只是再次從第一步開始重新嘗試。
在競爭不高時,基於 CAS 的計時器在效能上遠遠超過基於鎖的計數器。它的主要缺點在於,使用 CAS 操作我們需要自己處理競爭問題(例如以上程式碼中 compareAndSet 方法返回 false 的情況下,我們要再次迴圈嘗試),而使用鎖的情況下,這種競爭問題是底層自動處理的。
原子變數類
- 原子變數實際已經封裝了其所有需要的操作
鎖和原子變數的效能比較
比較結論是:在高度競爭的情況下,鎖的效能超過原子變數的效能;但在更真實的競爭情況下則反過來。原因是,高度競爭情況下,CAS 操作失敗之後會立即重試,這會導致更多的競爭,而鎖會掛起執行緒,降低 CPU 的使用率和共享記憶體總線上的通訊量。
這一點在 Redis 中也是同樣的,Redis 可以通過 WATCH 來實現 CAS 操作,繼而實現事務,但是在系統負載很重,主鍵爭用非常厲害的時候,用 WATCH/MULTI/EXEC 組成的樂觀鎖效能會嚴重下降,這種情況用悲觀鎖效率更高。
ABA 問題
ABA 問題存在於 CAS 操作中,如果我們的主執行緒第一次獲取了變數 V 的值 A,然後變數 V 先被其他執行緒改為值 B,又被改為值 A,那麼當我們主執行緒執行 CAS 操作的時候,發現變數 V 的值還是 A,並沒有發生變化,那這個判斷正確麼?
至少在某些情況下這是錯誤的,因此,我們可以額外維護一個原子變數 version(版本號),每次修改都會讓版本號遞增,我們可以通過配合版本號來判斷值是否發生變化。在 ElasticSearch 中,就是藉助 version 來實現 CAS 操作,也就是實現一個樂觀鎖。