線程安全是多線程領域的問題,線程安全可以簡單理解為一個方法或者一個實例可以在多線程環境中使用而不會出現問題。
在 Java 多線程編程當中,提供了多種實現 Java 線程安全的方式:
- 最簡單的方式,使用
Synchronization
關鍵字 - 使用
java.util.concurrent.atomic
包中的原子類,例如AtomicInteger
- 使用
java.util.concurrent.locks
包中的鎖 - 使用線程安全的集合
ConcurrentHashMap
- 使用
volatile
關鍵字,保證變量可見性(直接從內存讀,而不是從線程cache
讀
volatile 實現原理
- 在 JVM 底層 volatile 是采用“內存屏障”來實現的
- 緩存一致性協議(MESI協議)它確保每個緩存中使用的共享變量的副本是一致的。其核心思想如下:當某個 CPU 在寫數據時,如果發現操作的變量是共享變量,則會通知其他 CPU 告知該變量的緩存行是無效的,因此其他 CPU 在讀取該變量時,發現其無效會重新從主存中加載數據
synchronize 實現原理
同步代碼塊是使用 monitorenter
和 monitorexit
指令實現的,同步方法(在這看不出來需要看 JVM 底層實現)依靠的是方法修飾符上的 ACC_SYNCHRONIZED
實現。
CAS 樂觀鎖
CAS 是項樂觀鎖技術,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的沖突檢查 + 數據更新的原理是一樣的。
ABA 問題
CAS 會導致“ABA問題”。
CAS 算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較並替換,那么在這個時間差類會導致數據的變化。
比如說一個線程 one 從內存位置 V 中取出 A,這時候另一個線程 two 也從內存中取出 A,並且 two 進行了一些操作變成了 B,然后 two 又將 V 位置的數據變成 A,這時候線程 one 進行 CAS 操作發現內存中仍然是 A,然后 one 操作成功。盡管線程 one 的 CAS 操作成功,但是不代表這個過程就是沒有問題的。
部分樂觀鎖的實現是通過版本號(version)的方式來解決 ABA 問題,樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行 +1 操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現 ABA 問題,因為版本號只會增加不會減少。
樂觀鎖的業務場景及實現方式
樂觀鎖(Optimistic Lock):
- 每次獲取數據的時候,都不會擔心數據被修改,所以每次獲取數據的時候都不會進行加鎖,但是在更新數據的時候需要判斷該數據是否被別人修改過。如果數據被其他線程修改,則不進行數據更新,如果數據沒有被其他線程修改,則進行數據更新。由於數據沒有進行加鎖,期間該數據可以被其他線程進行讀寫操作。
- 比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,數據發生沖突的可能性就會增大,為了保證數據的一致性,應用層需要不斷的重新獲取數據,這樣會增加大量的查詢操作,降低了系統的吞吐量。