當進行多線程編程的時候,可能爭搶同一資源而引發不安全的問題。
多個線程訪問方法、某個實例對象出現問題——線程安全問題。
如果一次僅僅允許一個線程操作使用就不會發生問題,對於這種資源稱之爲臨界資源。
線程安全
線程安全是多線程場景下才會產生的問題,線程安全可以理解爲某個方法或者實例對象在多線程環境中使用而不會出現問題。
那麼怎麼解決線程安全問題呢?
線程安全解決方式
Java提供了這麼一些方式:
- 同步字
Synchronization
- 併發包
java.util.concurrent.atomic
裏面的原子類,例如AtomicInteger、AtomicBoolean等 - 併發包
java.util.concurrent.locks
裏面的鎖,如ReentrantLock、ReadWriteLock - 線程安全的集合類:
ConcurrentHashMap
、ConcurrentLinkedQueue
等 volatile
關鍵字,可以保證線程可見性、有序性(但是不保證原子性)
synchronize關鍵字
synchronize關鍵字是底層實現,通過monitorenter
、monitorexit
指令實現。可以通過字節碼查看到。
synchronize是同步鎖,也是可重入鎖。
synchronize的同步鎖,是互斥鎖、悲觀鎖,其他線程只能阻塞等待來獲得鎖。
synchronize的用法:
- 修飾實例方法
- 修飾類(類.class)對象、靜態方法
- 鎖定對象
原子類
通過原子類如AtomicInteger也能實現線程安全,底層是通過操作系統的CAS原子操作+自旋來實現的。
CAS
CAS操作是樂觀鎖的實現,當多個線程嘗試使用 CAS 同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS 操作包含三個操作數 —— 內存位置(V)、期望的原值(A)和要修改的目標新值(B)。如果內存中位置的值和期望原值A一樣,則更新爲B;否則不操作。
CAS是基於比較更新的操作,和數據庫實現的樂觀鎖很類似。
數據庫的樂觀鎖一般是增加一個冗餘字段(通常是行記錄的version),先查詢到version的原值v,更新時帶上version條件。
update 表名 set 字段=值, version=version+1 where version=v
溫馨提示:如果系統併發很高,數據庫樂觀鎖可能導致大量事務回滾,很多線程白乾活…
ABA問題
CAS存在ABA問題:CAS是先拿到原值,在去和內存中指定位置的現值比較,在這期間可能發生過變化,系統狀態可能發生了改變。
舉個栗子:一個線程拿到的是A,另一個線程也拿到了A並做了某些業務處理改爲了B,最後又改回了A,但是對於第一個線程來說,他通過CAS匹配是成立的,他不知道已經發生過系統狀態變更了,可能會引發某些問題。
洗錢案例:不法分子盜用你的賬號將1000萬轉走,把贓款1000萬打給你,對你而言還是賬內餘額1000萬,沒有變化,但是發生了洗錢行爲…
而上面提到的數據庫的樂觀鎖不會出現ABA問題,因爲version的值是不斷遞增的。
ABA問題解決
可以通過如:原子引用類解決,如 AtomicStampedReference
。
AtomicStampedReference 持有Integer的時間戳,可以根據時間戳比較判斷是否發生過改變——是不是和數據庫樂觀鎖實現方式類似了。
Lock
和synchronize是由JVM控制的不同,併發包裏面的鎖Lock,是從編程角度來解決臨界資源問題。
一般使用ReentrantLock
較多,多個線程使用同一個ReentrantLock
示例協調。可以手動控制加鎖、解鎖,常規使用方式如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // 獲得鎖
try {
// ...
} finally {
lock.unlock(); // 解鎖
}
}
}
在 JDK 1.5 中,synchronize
是性能低效的。因爲這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。
但是到了 JDK 1.6,發生了變化。synchronize
在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在 JDK 1.6 上 synchronize
的性能並不比 Lock
差。
volatile
volatile關鍵字可以實現可見性(線程數據從主內存獲取)、有序性(禁止指令重排)。