淺探CAS(Compare And Swap)實(shí)現(xiàn)原理
前言
CAS,全稱是Compare And Swap,即比較并交換,是一種樂(lè)觀鎖的實(shí)現(xiàn)。
悲觀鎖與樂(lè)觀鎖
悲觀鎖
總是假設(shè)最壞的情況,線程a每次去獲取或更新數(shù)據(jù)的時(shí)候,都會(huì)覺得別的線程也正在修改這個(gè)數(shù)據(jù),為了避免自己的更新操作丟失,線程a會(huì)嘗試獲取此數(shù)據(jù)的鎖,線程a獲取到之后,才能對(duì)此數(shù)據(jù)進(jìn)行一些更新操作。在此期間,別的線程無(wú)法更新,只能等到線程a釋放鎖之后,才能進(jìn)行更新。
之所以叫做悲觀鎖,是因?yàn)檫@是一種對(duì)數(shù)據(jù)的修改抱有悲觀態(tài)度的并發(fā)控制方式。我們一般認(rèn)為數(shù)據(jù)被并發(fā)修改的概率比較大,所以需要在修改之前先加鎖。
悲觀并發(fā)控制,實(shí)際上是一種“先取鎖再訪問(wèn)”的保守策略。
synchronized就是對(duì)悲觀鎖的一種實(shí)現(xiàn)。
樂(lè)觀鎖
樂(lè)觀鎖假設(shè)數(shù)據(jù)一般不會(huì)造成沖突,所以在拿數(shù)據(jù)的時(shí)候不會(huì)去加鎖,但是會(huì)在更新的時(shí)候判斷此期間內(nèi)有沒(méi)有別的線程修改過(guò)數(shù)據(jù)。
CAS機(jī)制就是對(duì)樂(lè)觀鎖的一種實(shí)現(xiàn)。
樂(lè)觀鎖的實(shí)現(xiàn)——CAS
CAS操作一般包含3個(gè)參數(shù),期望值、內(nèi)存值、新值。如果期望值與內(nèi)存值相等,則用新值去更新這個(gè)內(nèi)存值。如果不相等,則可以再次進(jìn)行比較,一直到成功為止。
CAS是一種非阻塞的算法,線程在更新失敗時(shí),不需要掛起,因此省去了大量線程上下文切換的開銷。
java使用Unsafe類來(lái)支持CAS操作,對(duì)Unsafe類不了解的同學(xué)可以先參考我的另外一篇文章JUC基石——Unsafe類。
我們用java代碼來(lái)簡(jiǎn)要模擬CAS的過(guò)程:
- /**
- * @param expect 期望值
- * @param update 新值
- * @return
- */
- public int cas(int expect, int update) {
- //更新失敗就一直進(jìn)行忙循環(huán)
- while (true) {
- //get方法從內(nèi)存中獲取最新的值
- int memory = get();
- if (memory == expect) {
- //set方法將內(nèi)存中的值設(shè)置為新值
- set(update);
- return update;
- }
- }
- }
當(dāng)然這只是一個(gè)模擬,實(shí)際cas操作將會(huì)用到底層的系統(tǒng)指令,這些指令將會(huì)保證整個(gè)cas操作具有原子性,關(guān)于這些指令,可能要另開篇幅講解。
悲觀鎖的實(shí)現(xiàn)——synchronized
synchronized是悲觀鎖的典型實(shí)現(xiàn),有關(guān)它的用法,可以參考我的這篇文章淺說(shuō)Synchronized,早期的synchronized十分笨重,所幸在1.6之后進(jìn)行了大量的優(yōu)化,鎖性能提升了很多,關(guān)于synchronized的優(yōu)化,可以參考我的這篇文章Synchronized的優(yōu)化。
CAS的缺陷——ABA問(wèn)題
假設(shè)有這樣的一種情況,x的內(nèi)存值首先是A,線程1讀取到了A,之后忙別的事情了,該值在之后被線程2改成了B,接著又被線程3改成了A,線程1此時(shí)進(jìn)行CAS操作,發(fā)現(xiàn)內(nèi)存值還是A,于是進(jìn)行了更新操作。但是這個(gè)A已經(jīng)不是原來(lái)的A了,或者說(shuō)不是之前那個(gè)版本的A了。
解決這種缺陷,可以使用帶版本號(hào)或時(shí)間戳的CAS,A值每次被更新后,版本號(hào)加1,或者更新時(shí)間戳。此時(shí)內(nèi)存值與期望值相等,但卻不是線程期望的版本號(hào)。
此時(shí)的A→B→A,就變成了A(version=1)→B(version=2)→A(version=3)。當(dāng)使用帶版本號(hào)的CAS后,就可以避免ABA問(wèn)題。
CAS與synchronized適用場(chǎng)景
線程沖突比較小時(shí),CAS進(jìn)行自旋操作,synchronized升級(jí)為輕量級(jí)鎖,也是在自旋,兩者的效率差不多。
線程沖突嚴(yán)重時(shí),CAS絕大部分的自旋操作將大量浪費(fèi)CPU的時(shí)間片,此時(shí)synchronized升級(jí)為重量級(jí)鎖,但在這種情況下,synchronized的效率遠(yuǎn)高于CAS。(因?yàn)樵诰€程沖突嚴(yán)重時(shí),synchronized已經(jīng)意識(shí)到輕量級(jí)鎖的自旋操作效率低下,主動(dòng)升級(jí)為重量級(jí)鎖,所以這里的忙循環(huán)的開銷遠(yuǎn)遠(yuǎn)大于線程切換的開銷)。
JAVA中的CAS操作
AtomicInteger實(shí)現(xiàn)了CAS,可以原子性地更新一個(gè)int類型數(shù)據(jù),其實(shí)底層也是調(diào)用Unsafe類。但是如果要一次原子性地更新多個(gè)變量,可以使用AtomicReference,當(dāng)然這個(gè)存在上述的ABA問(wèn)題,這時(shí)可以使用帶版本號(hào)機(jī)制的CAS實(shí)現(xiàn)類——AtomicStampedReference,該類使用了一個(gè)stamp字段來(lái)表示版本號(hào),代碼如下圖所示:

數(shù)據(jù)庫(kù)中的CAS操作
數(shù)據(jù)庫(kù)中的樂(lè)觀鎖機(jī)制不需要借助表鎖、行鎖等,以修改庫(kù)存為例,樂(lè)觀鎖實(shí)現(xiàn)如下:
- update goods set quantity=99 where id=1 and quantity = 100;
這個(gè)情景比較簡(jiǎn)單,暫不考慮ABA問(wèn)題。
以上SQL其實(shí)還是有一定的問(wèn)題的,就是一旦高并發(fā)的時(shí)候,就只有一個(gè)線程可以修改成功,那么就會(huì)存在大量的失敗。所以,需要減小樂(lè)觀鎖的粒度。
有一條比較好的建議,可以減小樂(lè)觀鎖力度,最大程度的提升吞吐率,提高并發(fā)能力!如下:
- update goods set quantity=quantity - 1 where id = 1 and quantity - 1 > 0
將quantity=100轉(zhuǎn)化成了quantity - 1 > 0,大大減少了樂(lè)觀鎖的力度,效率得到很大的提升。
JVM中的CAS操作
Java調(diào)用new object()會(huì)創(chuàng)建一個(gè)對(duì)象,這個(gè)對(duì)象會(huì)被分配到JVM的堆中。那么這個(gè)對(duì)象到底是怎么在堆中保存的呢?
首先,new object()執(zhí)行的時(shí)候,這個(gè)對(duì)象需要多大的空間,其實(shí)是已經(jīng)確定的,因?yàn)閖ava中的各種數(shù)據(jù)類型,占用多大的空間都是固定的。怎么去確定對(duì)象大小,可以參考我的這篇文章對(duì)象的內(nèi)存布局,怎樣確定對(duì)象的大小。那么接下來(lái)的工作就是在堆中找出那么一塊空間用于存放這個(gè)對(duì)象。
在單線程的情況下,一般有兩種分配策略:
- 指針碰撞:這種一般適用于內(nèi)存是絕對(duì)規(guī)整的(內(nèi)存是否規(guī)整取決于內(nèi)存回收策略)。用過(guò)的內(nèi)存放在一邊,空閑的內(nèi)內(nèi)存放在另外一邊,之間有一個(gè)分界指針,分配空間的工作只是將分界指針向空閑內(nèi)存一側(cè)移動(dòng)對(duì)象大小的距離即可。
- 空閑列表:這種適用于內(nèi)存非規(guī)整的情況,這種情況下JVM會(huì)維護(hù)一個(gè)內(nèi)存列表,記錄哪些內(nèi)存區(qū)域是空閑的,大小是多少。給對(duì)象分配空間的時(shí)候去空閑列表里查詢到合適的區(qū)域然后進(jìn)行分配即可。
然而,對(duì)象的創(chuàng)建工作是很頻繁的,為了保證效率,JVM可以并發(fā)地給對(duì)象分配內(nèi)存空間。由于分配內(nèi)存的時(shí)候不是原子性的操作,至少需要以下幾步:查找空閑列表、分配內(nèi)存、修改空閑列表等等,這是不安全的。解決并發(fā)時(shí)的安全問(wèn)題也有兩種策略:
- CAS:實(shí)際上虛擬機(jī)采用CAS配合上失敗重試的方式保證更新操作的原子性,原理和上面講的一樣。
- TLAB:如果使用CAS其實(shí)對(duì)性能還是會(huì)有影響的,所以JVM又提出了一種更高級(jí)的優(yōu)化策略:每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖區(qū)(TLAB),線程內(nèi)部需要分配內(nèi)存時(shí)直接在TLAB上分配就行,避免了線程沖突。只有當(dāng)緩沖區(qū)的內(nèi)存用光需要重新分配內(nèi)存的時(shí)候才會(huì)進(jìn)行CAS操作分配更大的內(nèi)存空間。 虛擬機(jī)是否使用TLAB,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)進(jìn)行配置(jdk5及以后的版本默認(rèn)是啟用TLAB的)。