cas技術(shù)守问,或者說設(shè)計(jì)思想,其實(shí)挺讓人著迷的体啰,至少對(duì)于我來說是如此荒勇,這些天一直在研究這一塊窿凤,今天寫寫針對(duì)cas的總結(jié),會(huì)持續(xù)更新品姓;
cas(compare and swap)植酥,中文翻譯比較替換,大家又將它談為樂觀鎖,說到鎖赏酥,關(guān)于java里面太多了搬素,而且還是面試官特別喜歡問的,簡(jiǎn)直讓人頭皮發(fā)麻猪杭,和樂觀鎖比較最多的就是悲觀鎖(synchronized)了。除此之外還有偏向鎖需纳,輕量級(jí)鎖口蝠,重量級(jí)鎖眉反,自旋鎖播歼,類鎖雇初,數(shù)據(jù)庫里面還有表鎖刊橘,行鎖嘴纺,elasticsearch里面還有全局鎖闲擦,文檔鎖俺榆,樹鎖萍桌,redis中的分布式鎖等等寇损;
我們平時(shí)應(yīng)用多線程技術(shù)非常多矛市,比如網(wǎng)上商店賣一類商品,多個(gè)用戶同時(shí)去購買,列車賣票着憨,多個(gè)用戶同時(shí)購買同一車次的車票等等場(chǎng)景,這些都涉及到了一個(gè)問題惧眠,共享資源暮顺,在多個(gè)線程去訪問共享資源的時(shí)候或链,如何去保證共享資源的一致性?在操作系統(tǒng)里面有一個(gè)臨界區(qū)的概念筛婉,其實(shí)就是一個(gè)程序片段,這個(gè)片段在同一時(shí)刻挑庶,針對(duì)共享資源,只能一個(gè)線程去訪問率挣,這就是鎖旱眯。那么這個(gè)程序片段。對(duì)應(yīng)java里面是什么呢惨奕?是synchronized雹洗,被synchronized修飾過的程序片段,相當(dāng)于加了鎖覆醇,而這個(gè)鎖鞋仍,就是悲觀鎖肚豺。那么問題來了蛉威,如果一個(gè)系統(tǒng)中的共享資源是改多讀少的情況敢朱,加鎖為了保證數(shù)據(jù)的一致性無可厚非构灸,但是如果這個(gè)系統(tǒng)中的共享資源是讀多寫少的情況呢?要知道曹阔,無論是讀半开,還是寫,進(jìn)入該程序片段赃份,都是要加鎖的寂拆,而每加鎖一次這涉及到了系統(tǒng)調(diào)用內(nèi)核態(tài)和用戶態(tài)之間的切換,頻繁加鎖抓韩,釋放鎖纠永,這個(gè)代價(jià)是非常大的,所以我們提出了樂觀鎖的概念谒拴,樂觀鎖在應(yīng)用層尝江,其實(shí)是沒有鎖的。這也是CAS技術(shù)為什么多用于讀多寫少的應(yīng)用場(chǎng)景英上。
CAS是怎么做到?jīng)]有加鎖就能保證數(shù)據(jù)的一致性的呢茂装?
首先怠蹂,CAS有三個(gè)概念,分別是V少态,A,B易遣;V表示要修改的值彼妻,即共享資源,這個(gè)值可以是內(nèi)存中豆茫,可以是數(shù)據(jù)庫中侨歉,總之這個(gè)是你的共享資源,A表示更新的比較預(yù)期值揩魂,B表示即將要更新的新值幽邓。這么說可能有點(diǎn)不太理解,那么說說cas的操作流程就明白了火脉。流程:V為共享資源牵舵,多個(gè)線程要去操作V,每個(gè)線程會(huì)先讀取V的值倦挂,將值賦給A畸颅,其中一個(gè)線程要執(zhí)行更新操作,這個(gè)時(shí)候方援,會(huì)將這個(gè)線程的A值和現(xiàn)在內(nèi)存中的V值對(duì)比没炒,如果沒有變化,說明這個(gè)之前沒有別的線程修改(這里不涉及ABA問題犯戏,ABA問題可以通過時(shí)間戳或者版本解決)送火,于是將V的值更新為B,如果A值和V值不相等先匪,則不更新或者重新獲取A值种吸。
這就是CAS的設(shè)計(jì),不過胚鸯,看到這里骨稿,大家有沒有想過,如果有兩個(gè)或者多個(gè)線程同時(shí)都經(jīng)過了第一次的V和A值比較呢姜钳?那么到了更新一步是不是會(huì)出現(xiàn)數(shù)據(jù)不一致的情況坦冠?因?yàn)楸容^和更新其實(shí)并不是原子操作。
我們來討論一下JAVA 8中是怎么解決這個(gè)問題的哥桥。
比較典型的原子操作對(duì)象辙浑,AtomicInteger,我們可以看一下里面的源碼拟糕,自增1是怎么實(shí)現(xiàn)的
public final int incrementAndGet() {
? ? for (;;) {
? ? ? ? int current = get();
? ? ? ? int next = current + 1;
? ? ? ? if (compareAndSet(current, next))
? ? ? ? ? ? return next;
? ? }
}
可以看到判呕,首先get()從內(nèi)存中獲取內(nèi)存值倦踢,在使用之前,你要自己聲明一個(gè)volatile的所有線程可見變量侠草。接著辱挥,加1賦值給了next,接著將current和next傳給了compareAndSet函數(shù)边涕,如果成功晤碘,則返回next值,這個(gè)時(shí)候功蜓,current和next就是分別對(duì)應(yīng)上文的A园爷,B。繼續(xù)看compareAndSet是怎么保證原子操作的式撼。
public final boolean compareAndSet(int expect, int update) {?
? ? return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
? ? }
可以看到童社,這里直接把值傳給了unsafe的compareAndSwapInt方法,由它來保證原子操作著隆,這里我就不貼源碼分析了扰楼。我直接總結(jié),這個(gè)unsafe其實(shí)是java中的jni(java native interface)技術(shù)調(diào)用另外語言的實(shí)現(xiàn)旅东,這里是*.cpp灭抑,名字太長,我用*代替抵代,這個(gè)操作呢其實(shí)是硬件層面的技術(shù)腾节,涉及到底層的匯編,總的來說就是進(jìn)入這個(gè)unsafe的函數(shù)后荤牍,在計(jì)算機(jī)內(nèi)存運(yùn)行中案腺,同一時(shí)刻通過總線和緩存的鎖定,從而實(shí)現(xiàn)了比較和替換康吵,達(dá)到原子操作的目的劈榨,不得不說,看*.cpp文件是一件掉發(fā)的操作晦嵌,珍愛發(fā)型同辣,遠(yuǎn)離禿頭;
說了半天惭载,原來這里面最難的原子操作是在硬件層面呀旱函,那我們有沒有可能在應(yīng)用層就能實(shí)現(xiàn)呢?答案自然是有的描滔,這里我提供一個(gè)我自己的個(gè)人想法棒妨;
這里我采用的做法是調(diào)用已經(jīng)寫好的AtomicInteger來輔助。
1含长、聲明一個(gè)針對(duì)該共享資源的volatile int變量value券腔,初始化為0伏穆;
2、兩個(gè)或者多個(gè)線程進(jìn)入了第一步的V和A比較纷纫,獲取value值枕扫,這個(gè)時(shí)候,多個(gè)線程就變成了串行化涛酗,而不用加鎖铡原,因?yàn)関alue是嚴(yán)格排序的;
3商叹、獲得value為1的線程執(zhí)行更新操作,操作完之后只泼,初始化為0剖笙,其余不為了1的線程重新獲取V值或者處理丟棄;
那么還有一個(gè)問題请唱,如果獲取到1的線程在執(zhí)行過程中死掉了怎么辦弥咪?如果死掉了,那么豈不是這里一直就停住了十绑,不能操作共享資源啦聚至?因?yàn)橹挥衯alue為1的線程才能進(jìn)入臨界區(qū),所以本橙,在這里扳躬,我們還要設(shè)置一個(gè)超時(shí)時(shí)間,如果超過設(shè)定時(shí)間甚亭,還是要?dú)⒌衄F(xiàn)在操作資源的線程贷币,重置value值為0和復(fù)原在此過程中改變的V值,以便讓其他線程進(jìn)入亏狰,當(dāng)然這種情況幾乎很少發(fā)生役纹,但還是得考慮。
ABA問題就是線程1的A值去比較V值暇唾,發(fā)現(xiàn)相等促脉,但是如果在這之前,線程2將V值改了策州,線程3又將V值改回來瘸味,那這樣其實(shí)A值和V值是相等的,但其實(shí)里面已經(jīng)有了變動(dòng)抽活,所以叫ABA問題硫戈,可以采用時(shí)間戳,在對(duì)比那一步下硕,加入了時(shí)間戳對(duì)比就可以了,或者如果是elasticsearch中可以采用版本號(hào)比較。
cas應(yīng)用層實(shí)現(xiàn)做法分唾,大家也可以去看看disruptor框架的做法沧踏,里面提出了一個(gè)前門后門的概念,采用環(huán)形緩存做法洗搂,據(jù)說非常高效;
如果還有別的地方?jīng)]有考慮到,歡迎大家提出铸题,一起學(xué)習(xí)進(jìn)步。