我們知道悲觀鎖在高并發(fā)的場(chǎng)景下,激烈的鎖競(jìng)爭(zhēng)會(huì)造成線程阻塞,大量阻塞線程會(huì)導(dǎo)致系統(tǒng)上下文切換,增加系統(tǒng)的性能開(kāi)銷。那么有沒(méi)有可能實(shí)現(xiàn)一種非阻塞的鎖機(jī)制來(lái)保證線程的安全呢?答案是肯定的。今天我就帶你學(xué)習(xí)下樂(lè)觀鎖的優(yōu)化方法,看看怎么使用才能發(fā)揮它最大的價(jià)值。
一 什么是樂(lè)觀鎖
樂(lè)觀鎖,顧名思義,就是說(shuō)在操作共享資源時(shí),它總是抱著樂(lè)觀的態(tài)度進(jìn)行,它認(rèn)為自己可以成功的完成操作。但實(shí)際上,當(dāng)多個(gè)線程同時(shí)操作一個(gè)共享資源時(shí),只有一個(gè)線程會(huì)成功,那么失敗的線程呢?它們不會(huì)像悲觀鎖一樣,在操作系統(tǒng)中掛起,而僅僅是返回,并且系統(tǒng)允許失敗的線程重試,也允許自動(dòng)放棄退出操作。
所以,樂(lè)觀鎖相比悲觀鎖來(lái)說(shuō),不會(huì)帶來(lái)死鎖,饑餓等活性故障問(wèn)題,線程間的相互影響也遠(yuǎn)遠(yuǎn)比悲觀鎖要小。更為重要的是,樂(lè)觀鎖沒(méi)有因鎖競(jìng)爭(zhēng)造成的系統(tǒng)開(kāi)銷,所以在性能上也是更勝一籌。
二 樂(lè)觀鎖的實(shí)現(xiàn)原理
CAS是實(shí)現(xiàn)樂(lè)觀鎖的核心算法,它包含了3個(gè)參數(shù):V(需要更新的變量),E(預(yù)期值)和N(最新值)。
1.CAS如何實(shí)現(xiàn)原子操作
在JDK中的concurrent包中,atomic路徑下的類都是基于CAS實(shí)現(xiàn)的。AtomicInteger就是基于CAS實(shí)現(xiàn)的一個(gè)線程安全的整型類。下面我們通過(guò)源碼來(lái)了解下如何使用CAS實(shí)現(xiàn)原子操作。
我們可以看到AtomicInteger的自增方法是使用了Unsafe的getAndAddInt方法,顯然AtomicInteger依賴于本地方法Unsafe類,Unsafe類中的操作方法會(huì)調(diào)用CPU底層指令實(shí)現(xiàn)原子操作。

2.處理器如何實(shí)現(xiàn)原子操作
CAS是調(diào)用處理器底層指令來(lái)實(shí)現(xiàn)原子操作,那么處理器底層又是如何實(shí)現(xiàn)原子操作的呢?
處理器和物理內(nèi)存之間的通信速度要遠(yuǎn)慢于處理器間的處理速度,所以處理器有自己的內(nèi)部緩存。如下圖所示,在執(zhí)行操作時(shí),頻繁使用的內(nèi)存數(shù)據(jù)會(huì)緩存在處理器的L1,L2和L3高速緩存中,以加快頻繁讀取的速度。

三 優(yōu)化CAS樂(lè)觀鎖
雖然樂(lè)觀鎖在并發(fā)性能上要比悲觀蘇優(yōu)越,但是在于寫(xiě)大于讀的操作場(chǎng)景下,CAS失敗的可能性會(huì)增大,如果不放棄此次CAS操作,就需要循環(huán)做CAS重試,這無(wú)疑會(huì)長(zhǎng)時(shí)間地占用CPU。
在JAVA1.7中,通過(guò)以下代碼我們可以看到:AtomicInteger的getAndSet方法中使用了for循環(huán)不斷重試CAS操作,如果長(zhǎng)時(shí)間不成功,就會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷。到了Java8,for循環(huán)雖然被去掉了,但是我們反編譯Unsafe類時(shí)就可以發(fā)現(xiàn)該循環(huán)其實(shí)是被封裝在了Unsafe類中,CPU的執(zhí)行開(kāi)銷依然存在。

JDK1.8中,Java提供了一個(gè)新的原子類LongAdder,LongAdder在高并發(fā)的場(chǎng)景下比AtomicInteger和AtomicLong的性能更好,代價(jià)就是會(huì)消耗更多的內(nèi)存空間。
LongAdder內(nèi)部由一個(gè)base變量和一個(gè)cell[]數(shù)組組成。當(dāng)只有一個(gè)寫(xiě)線程,沒(méi)有競(jìng)爭(zhēng)的情況下,LongAdder會(huì)直接使用base變量作為原子操作變量,通過(guò)CAS操作修改變量;當(dāng)有多個(gè)寫(xiě)線程競(jìng)爭(zhēng)的情況下,除了占用base變量的一個(gè)寫(xiě)線程之外,其它各個(gè)線程會(huì)將修改的變量寫(xiě)入到自己的槽cell[]數(shù)組中,最終結(jié)果可通過(guò)公式計(jì)算得出:

四 總結(jié)
在日常開(kāi)發(fā)中,使用樂(lè)觀鎖最常見(jiàn)的場(chǎng)景就是數(shù)據(jù)庫(kù)的更新操作了。為了保證操作數(shù)據(jù)庫(kù)的原子性,我們常常會(huì)為每一條數(shù)據(jù)定義一個(gè)版本號(hào),并在更新前獲取到它,到了更新數(shù)據(jù)庫(kù)的時(shí)候,還要判斷下已經(jīng)獲取的版本號(hào)是否被更新過(guò),如果沒(méi)有,則執(zhí)行該操。
CAS樂(lè)觀鎖在平常使用時(shí)比較受限,它只能保證單個(gè)變量操作的原子性,當(dāng)涉及到多個(gè)變量時(shí),CAS就無(wú)能為力了,但前兩講講到的悲觀鎖可以通過(guò)對(duì)整個(gè)代碼塊加鎖來(lái)做到這點(diǎn)。
CAS樂(lè)觀鎖在高并發(fā)寫(xiě)大于讀的場(chǎng)景下,大部分線程的原子操作會(huì)失敗,失敗后的線程將會(huì)不斷重試CAS原子操作,這樣就會(huì)導(dǎo)致大量線程長(zhǎng)時(shí)間地占用CPU資源,給系統(tǒng)帶來(lái)很大的性能開(kāi)銷。在JDK1.8中,Java新增了一個(gè)原子類LongAdder,它使用了空間換時(shí)間的方法,解決了上訴問(wèn)題。
最近的這幾講中,我詳細(xì)的講解了基于JVM實(shí)現(xiàn)的同步鎖Sychronized,AQS實(shí)現(xiàn)的同步鎖Lock以及CAS實(shí)現(xiàn)的樂(lè)觀鎖。相信你也很好奇,這三種鎖,到底哪一種的性能最好,現(xiàn)在我們來(lái)對(duì)比一下不同實(shí)現(xiàn)方式下的鎖的性能。
鑒于脫離實(shí)際業(yè)務(wù)場(chǎng)景的性能對(duì)比測(cè)試結(jié)果沒(méi)有意義,我們可以分別在“讀多寫(xiě)少”,“讀少寫(xiě)多”,“讀寫(xiě)差不多”這三種場(chǎng)景下進(jìn)行測(cè)試。又因?yàn)殒i的性能還與競(jìng)爭(zhēng)的激烈程度有關(guān),所以除此之外,我們還將做三種鎖在不同競(jìng)爭(zhēng)級(jí)別下的性能測(cè)試。
綜合上述條件,我將對(duì)四種模式下的五個(gè)鎖Sychronized,ReentrantLock,ReentrantReadWriteLock,StampedLock以及樂(lè)觀鎖LongAdder進(jìn)行壓測(cè)。

通過(guò)以上結(jié)果,我們可以發(fā)現(xiàn):在讀大于寫(xiě)的場(chǎng)景下,讀寫(xiě)鎖ReentrantReadWriteLock,StampedLock以及樂(lè)觀鎖的讀寫(xiě)性能是最好的;在寫(xiě)大于讀的場(chǎng)景下,樂(lè)觀鎖的性能是最好的,其它4種鎖的性能則差不多;在讀和寫(xiě)差不多的場(chǎng)景下,兩種讀寫(xiě)鎖以及樂(lè)觀鎖的性能要優(yōu)于Sychronized和ReentrantLock。






