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