我們知道悲觀鎖在高并發的場景下,激烈的鎖競爭會造成線程阻塞,大量阻塞線程會導致系統上下文切換,增加系統的性能開銷。那么有沒有可能實現一種非阻塞的鎖機制來保證線程的安全呢?答案是肯定的。今天我就帶你學習下樂觀鎖的優化方法,看看怎么使用才能發揮它最大的價值。
一 什么是樂觀鎖
樂觀鎖,顧名思義,就是說在操作共享資源時,它總是抱著樂觀的態度進行,它認為自己可以成功的完成操作。但實際上,當多個線程同時操作一個共享資源時,只有一個線程會成功,那么失敗的線程呢?它們不會像悲觀鎖一樣,在操作系統中掛起,而僅僅是返回,并且系統允許失敗的線程重試,也允許自動放棄退出操作。
所以,樂觀鎖相比悲觀鎖來說,不會帶來死鎖,饑餓等活性故障問題,線程間的相互影響也遠遠比悲觀鎖要小。更為重要的是,樂觀鎖沒有因鎖競爭造成的系統開銷,所以在性能上也是更勝一籌。
二 樂觀鎖的實現原理
CAS是實現樂觀鎖的核心算法,它包含了3個參數:V(需要更新的變量),E(預期值)和N(最新值)。
1.CAS如何實現原子操作
在JDK中的concurrent包中,atomic路徑下的類都是基于CAS實現的。AtomicInteger就是基于CAS實現的一個線程安全的整型類。下面我們通過源碼來了解下如何使用CAS實現原子操作。
我們可以看到AtomicInteger的自增方法是使用了Unsafe的getAndAddInt方法,顯然AtomicInteger依賴于本地方法Unsafe類,Unsafe類中的操作方法會調用CPU底層指令實現原子操作。
2.處理器如何實現原子操作
CAS是調用處理器底層指令來實現原子操作,那么處理器底層又是如何實現原子操作的呢?
處理器和物理內存之間的通信速度要遠慢于處理器間的處理速度,所以處理器有自己的內部緩存。如下圖所示,在執行操作時,頻繁使用的內存數據會緩存在處理器的L1,L2和L3高速緩存中,以加快頻繁讀取的速度。
三 優化CAS樂觀鎖
雖然樂觀鎖在并發性能上要比悲觀蘇優越,但是在于寫大于讀的操作場景下,CAS失敗的可能性會增大,如果不放棄此次CAS操作,就需要循環做CAS重試,這無疑會長時間地占用CPU。
在JAVA1.7中,通過以下代碼我們可以看到:AtomicInteger的getAndSet方法中使用了for循環不斷重試CAS操作,如果長時間不成功,就會給CPU帶來非常大的執行開銷。到了Java8,for循環雖然被去掉了,但是我們反編譯Unsafe類時就可以發現該循環其實是被封裝在了Unsafe類中,CPU的執行開銷依然存在。
JDK1.8中,Java提供了一個新的原子類LongAdder,LongAdder在高并發的場景下比AtomicInteger和AtomicLong的性能更好,代價就是會消耗更多的內存空間。
LongAdder內部由一個base變量和一個cell[]數組組成。當只有一個寫線程,沒有競爭的情況下,LongAdder會直接使用base變量作為原子操作變量,通過CAS操作修改變量;當有多個寫線程競爭的情況下,除了占用base變量的一個寫線程之外,其它各個線程會將修改的變量寫入到自己的槽cell[]數組中,最終結果可通過公式計算得出:
四 總結
在日常開發中,使用樂觀鎖最常見的場景就是數據庫的更新操作了。為了保證操作數據庫的原子性,我們常常會為每一條數據定義一個版本號,并在更新前獲取到它,到了更新數據庫的時候,還要判斷下已經獲取的版本號是否被更新過,如果沒有,則執行該操。
CAS樂觀鎖在平常使用時比較受限,它只能保證單個變量操作的原子性,當涉及到多個變量時,CAS就無能為力了,但前兩講講到的悲觀鎖可以通過對整個代碼塊加鎖來做到這點。
CAS樂觀鎖在高并發寫大于讀的場景下,大部分線程的原子操作會失敗,失敗后的線程將會不斷重試CAS原子操作,這樣就會導致大量線程長時間地占用CPU資源,給系統帶來很大的性能開銷。在JDK1.8中,Java新增了一個原子類LongAdder,它使用了空間換時間的方法,解決了上訴問題。
最近的這幾講中,我詳細的講解了基于JVM實現的同步鎖Sychronized,AQS實現的同步鎖Lock以及CAS實現的樂觀鎖。相信你也很好奇,這三種鎖,到底哪一種的性能最好,現在我們來對比一下不同實現方式下的鎖的性能。
鑒于脫離實際業務場景的性能對比測試結果沒有意義,我們可以分別在“讀多寫少”,“讀少寫多”,“讀寫差不多”這三種場景下進行測試。又因為鎖的性能還與競爭的激烈程度有關,所以除此之外,我們還將做三種鎖在不同競爭級別下的性能測試。
綜合上述條件,我將對四種模式下的五個鎖Sychronized,ReentrantLock,ReentrantReadWriteLock,StampedLock以及樂觀鎖LongAdder進行壓測。
通過以上結果,我們可以發現:在讀大于寫的場景下,讀寫鎖ReentrantReadWriteLock,StampedLock以及樂觀鎖的讀寫性能是最好的;在寫大于讀的場景下,樂觀鎖的性能是最好的,其它4種鎖的性能則差不多;在讀和寫差不多的場景下,兩種讀寫鎖以及樂觀鎖的性能要優于Sychronized和ReentrantLock。