synchronized / Lock / CAS
- synchronized和Lock實現的同步鎖機制,都屬於悲觀鎖,而CAS屬於樂觀鎖
- 悲觀鎖在高併發的場景下,激烈的鎖競爭會形成線程阻塞,而大量阻塞線程會致使系統的上下文切換,增長系統的性能開銷
樂觀鎖
- 樂觀鎖:在操做共享資源時,老是抱着樂觀的態度進行,認爲本身可以完成操做
- 但實際上,當多個線程同時操做一個共享資源時,只有一個線程會成功,失敗的線程不會被掛起,僅僅只是返回
- 樂觀鎖相比於悲觀鎖來講,不會帶來死鎖、飢餓等活性故障問題,線程間的相互影響也遠遠比悲觀鎖要小
- 樂觀鎖沒有因競爭而形成的系統上下文切換,因此在性能上更勝一籌
實現原理
- CAS是實現樂觀鎖的核心算法,包含3個參數:V(須要更新的變量),E(預期值)、N(最新值)
- 只有V等於E時,V纔會被設置爲N
- 若是V不等於E了,說明其它線程已經更新了V,此時該線程不作操做,返回V的真實值
CAS實現原子操做
AtomicInteger是基於CAS實現的一個線程安全的整型類,Unsafe調用CPU底層指令實現原子操做java
處理器實現原子操做
- CAS是調用處理器底層指令來實現原子操做的
- 處理器和物理內存之間的通訊速度要遠低於處理器間的處理速度,因此處理器有本身的內部緩存(L1/L2/L3)
- 服務器一般爲多處理器,而且處理器是多核的,每一個處理器維護了一塊字節的緩存存,每一個內核也維護了一塊字節的緩存
- 此時在多線程併發就會存在緩存不一致的問題,從而致使數據不一致
- 處理器提供了總線鎖定和緩存鎖定兩種機制來保證複雜內存操做的原子性
- 總線鎖定
- 當處理器要操做一個共享變量時,會在總線上會發出一個Lock信號,此時其它處理器就不能操做共享變量了
- 總線鎖定在阻塞其餘處理器獲取該共享變量的操做請求時,也可能會致使大量阻塞,從而增長系統的性能開銷
- 緩存鎖定(後來出現)
- 當某個處理器對緩存中的共享變量進行了操做,就會通知其餘處理器放棄存儲或者從新讀取該共享變量
- 目前最新的處理器都支持緩存鎖定機制
優化CAS樂觀鎖
- 樂觀鎖在併發性能上要優於悲觀鎖
- 但在寫大於讀的操做場景下,CAS失敗的可能性增大,若是循環CAS,會長時間佔用CPU
- 例如上面的AtomicInteger#getAndIncrement
- JDK 1.8中,提供了新的原子類LongAdder
- LongAdder在高併發場景下會比AtomicInteger和AtomicLong的性能更好,代價是消耗更多的內存空間
- 核心思想:空間換時間
- 實現原理:下降操做共享變量的併發數
- LongAdder內部由一個base變量和一個cell[]數組組成
- 當只有一個寫線程(沒有競爭)
- LongAdder會直接使用base變量做爲原子操做變量,經過CAS操做修改base變量
- 當有多個寫線程(存在競爭)
- 除了佔用base變量的一個寫線程外,其餘寫線程的value值會分散到cell數組中
- 不一樣線程會命中到數組的不一樣槽中,各個線程只對本身槽中的value進行CAS操做
- value=base+∑ni=0Cell[i]value=base+∑i=0nCell[i]
- LongAdder在操做後的返回值只是一個近似準確的值,但最終返回的是一個準確的值
性能對比
- 讀大於寫,讀寫鎖ReentrantReadWriteLock、讀寫鎖StampedLock、樂觀鎖LongAdder的性能最好
- 寫大於讀,樂觀鎖的性能最好,其餘四種鎖的性能差很少
- 讀約等於寫,兩種讀寫鎖和樂觀鎖的性能要優於synchronized和Lock
小結
- 樂觀鎖的常見使用場景:數據庫更新
- 爲每條數據定義一個版本號,在更新前獲取版本號,在更新數據時,再判斷版本號是否被更新過,若是沒有才更新數據
- CAS樂觀鎖的使用比較受限,由於樂觀鎖只能保證單個變量操做的原子性
- CAS樂觀鎖在高併發寫大於讀的場景下
- 大部分線程的原子操做會失敗,失敗後的線程將不斷重試CAS原子操做,致使大量線程長時間佔用CPU資源
- JDK 1.8中,新增了原子類LongAdder,採用空間換時間的思路解決了這個問題,但實時性不高