CAS樂觀鎖原理

1. 樂觀鎖和悲觀鎖

悲觀鎖

總是假設(shè)最壞的情況,為防止每次去拿數(shù)據(jù)別人修改镜遣,每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖(共享資源每次只給一個(gè)線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)边翼。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖鸣剪,表鎖等,讀鎖丈积,寫鎖等筐骇,都是在做操作之前先上鎖。Java中synchronizedReentrantLock等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)江滨。
Synchronized雖然確保了線程的安全铛纬,但是在性能上卻不是最優(yōu)的,Synchronized關(guān)鍵字會(huì)讓沒有得到鎖資源的線程進(jìn)入BLOCKED狀態(tài)唬滑,而后在爭(zhēng)奪到鎖資源后恢復(fù)為RUNNABLE狀態(tài)告唆,這個(gè)過程中涉及到操作系統(tǒng)用戶模式和內(nèi)核模式的轉(zhuǎn)換棺弊,代價(jià)比較高鞍盗。

樂觀鎖

總是假設(shè)最好的情況诸迟,每次拿數(shù)據(jù)的時(shí)候認(rèn)為別人不會(huì)更改數(shù)據(jù)内边,所以不對(duì)數(shù)據(jù)加鎖赐写,但是在更新數(shù)據(jù)的時(shí)候都要先判定一下這個(gè)數(shù)據(jù)在計(jì)算期間有沒有被別人修改(例如:判斷值是否與之前的一致)觅闽,如果被修改則再次重試計(jì)算巷查。
CAS(Compare and Swap疯坤,即比較再交換)是樂觀鎖的典型實(shí)現(xiàn)编曼,當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí)僧凤,只有其中一個(gè)線程能更新變量的值畜侦,而其它線程都失敗,失敗的線程并不會(huì)被掛起躯保,而是被告知這次競(jìng)爭(zhēng)中失敗旋膳,并可以再次嘗試。

2. 什么是CAS途事?

CAS機(jī)制

CAS機(jī)制當(dāng)中使用了3個(gè)基本操作數(shù):內(nèi)存地址V验懊,舊的值A(chǔ),要修改的新值B盯孙。

CAS原理

更新一個(gè)變量的時(shí)候鲁森,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí),才會(huì)將內(nèi)存地址V對(duì)應(yīng)的值修改振惰。

借用別人的一段代碼歌溉,感覺很形象,可以用來模擬CAS的過程:

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author hrabbit
 * 2018/07/16.
 */
public class AtomicBooleanTest implements Runnable {

    private static AtomicBoolean flag = new AtomicBoolean(true);

    public static void main(String[] args) {
        AtomicBooleanTest ast = new AtomicBooleanTest();
        Thread thread1 = new Thread(ast);
        Thread thread = new Thread(ast);
        thread1.start();
        thread.start();
    }
    @Override
    public void run() {
        System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
        if (flag.compareAndSet(true,false)){
            System.out.println(Thread.currentThread().getName()+""+flag.get());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag.set(true);
        }else{
            System.out.println("重試機(jī)制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            run();
        }

    }
}

我們來看上面這段代碼骑晶,同時(shí)啟動(dòng)兩個(gè)線程痛垛,同時(shí)去更改AtomicBoolean 類型的屬性flag的值(通過flag.compareAndSet(true,false)將true值更新成false)。compareAndSet的官方解釋如下:

compareAndSet

也就是說當(dāng)flag的值為true的時(shí)候桶蛔,會(huì)觸發(fā)更新操作將flag的值更新成false匙头。一旦有一個(gè)線程執(zhí)行了這個(gè)操作,其他線程將會(huì)等待if條件成立才會(huì)去執(zhí)行仔雷。

最終的輸出:

thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false

通過以上的輸出可以看到蹂析,兩個(gè)線程Thread-0和Thread-1同時(shí)執(zhí)行run函數(shù),但是Thread1先執(zhí)行了flag.compareAndSet(true,false),更新了flag的值碟婆,這時(shí)候Thread1進(jìn)入sleep电抚,Thread0一直沒有進(jìn)入if的條件,當(dāng)Thread1 sleep完之后竖共,重置flag的值到true蝙叛,這時(shí)候flag.compareAndSet(true,false)的條件滿足,Thread0才可以進(jìn)入if執(zhí)行公给。我們看到flag.compareAndSet(true,false)flag.set(true)相當(dāng)于一把鎖借帘,鎖住了中間的代碼蜘渣。

CAS的缺點(diǎn)

1.CPU開銷較大
在并發(fā)量比較高的情況下,如果許多線程反復(fù)嘗試更新某一個(gè)變量肺然,卻又一直更新不成功蔫缸,循環(huán)往復(fù),會(huì)給CPU帶來很大的壓力狰挡。

2.不能保證代碼塊的原子性
CAS機(jī)制所保證的只是一個(gè)變量的原子性操作捂龄,而不能保證整個(gè)代碼塊的原子性。比如需要保證3個(gè)變量共同進(jìn)行原子性的更新加叁,就不得不使用Synchronized了倦沧。

CAS對(duì)比Synchronized:

  • 對(duì)于資源競(jìng)爭(zhēng)較少(線程沖突較輕)的情況,使用synchronized同步鎖進(jìn)行線程阻塞和喚醒切換以及用戶態(tài)內(nèi)核態(tài)間的切換操作額外浪費(fèi)消耗cpu資源它匕;而CAS基于硬件實(shí)現(xiàn)展融,不需要進(jìn)入內(nèi)核,不需要切換線程豫柬,操作自旋幾率較少告希,因此可以獲得更高的性能。

  • 對(duì)于資源競(jìng)爭(zhēng)嚴(yán)重(線程沖突嚴(yán)重)的情況烧给,CAS自旋的概率會(huì)比較大燕偶,從而浪費(fèi)更多的CPU資源,效率低于synchronized础嫡。

補(bǔ)充: synchronized在jdk1.6之后指么,已經(jīng)改進(jìn)優(yōu)化。synchronized的底層實(shí)現(xiàn)主要依靠Lock-Free的隊(duì)列榴鼎,基本思路是自旋后阻塞伯诬,競(jìng)爭(zhēng)切換后繼續(xù)競(jìng)爭(zhēng)鎖,稍微犧牲了公平性巫财,但獲得了高吞吐量盗似。在線程沖突較少的情況下,可以獲得和CAS類似的性能平项;而線程沖突嚴(yán)重的情況下赫舒,性能遠(yuǎn)高于CAS。

Atomic類

在java.util.concurrent.atomic包下闽瓢,一系列以Atomic開頭的包裝類都是能夠保證原子性操作的号阿。例如AtomicBooleanAtomicInteger鸳粉,AtomicLong。它們分別用于Boolean园担,Integer届谈,Long類型的原子性操作枯夜。
先看個(gè)例子:


    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每個(gè)線程讓count自增100次
                    for (int i = 0; i < 100; i++) {
                        count.incrementAndGet();
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

使用AtomicInteger之后,最終的輸出結(jié)果可以保證是200艰山,效果跟sychronized是一樣的湖雹,但是在某些情況下,代碼的性能會(huì)比Synchronized更好曙搬。

atomic操作的底層實(shí)現(xiàn)正是利用的CAS機(jī)制摔吏。

AtomicReference

AtomicReference類提供了一種讀和寫都是原子性的對(duì)象引用變量。原子意味著多個(gè)線程試圖改變同一個(gè)AtomicReference(例如比較和交換操作)將不會(huì)使得AtomicReference處于不一致的狀態(tài)纵装。

AtomicReference有一個(gè)非常有用的方法是compareAndSet()征讲。compareAndSet()方法可以將存儲(chǔ)在AtomicReference中的引用同你的預(yù)期值做一個(gè)比較,如果他們是相同的(not equal as in equals() but same as in ==)橡娄,那么在AtomicReference實(shí)例上會(huì)設(shè)置一個(gè)新的引用诗箍。

atomic的源碼

我們可以看到java.util.concurrent.atomic包下面各個(gè)類的源碼都比較簡(jiǎn)單,主要利用了sun.misc.Unsafe實(shí)現(xiàn)的CAS原理挽唉,其中value用volatile聲明滤祖,使得value對(duì)各個(gè)線程可見。valueOffset為內(nèi)存地址的偏移量瓶籽。sun.misc.Unsafe里關(guān)于對(duì)象字段訪問的方法把對(duì)象布局抽象出來匠童,它提供了objectFieldOffset()方法用于獲取某個(gè)字段相對(duì)Java對(duì)象的“起始地址”的偏移量。

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

//...省略若干行

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

sun.misc.Unsafe是JDK內(nèi)部用的工具類塑顺。它通過暴露一些Java意義上說“不安全”的功能給Java層代碼汤求,來讓JDK能夠更多的使用Java代碼來實(shí)現(xiàn)一些原本是平臺(tái)相關(guān)的、需要使用native語言(例如C或C++)才可以實(shí)現(xiàn)的功能茬暇。該類不應(yīng)該在JDK核心類庫之外使用首昔。

我們?cè)賮砜聪耮et和set方法:

  /**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }

    /**
     * Sets to the given value.
     *
     * @param newValue the new value
     */
    public final void set(int newValue) {
        value = newValue;
    }

我們看到get和set方法沒有調(diào)用unsafe的方法。

參考文獻(xiàn):
https://www.imooc.com/article/details/id/44217
https://www.cnblogs.com/snow-man/p/9970523.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糙俗,一起剝皮案震驚了整個(gè)濱河市勒奇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌巧骚,老刑警劉巖赊颠,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異劈彪,居然都是意外死亡竣蹦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門沧奴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痘括,“玉大人,你說我怎么就攤上這事「倬” “怎么了挠日?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)翰舌。 經(jīng)常有香客問我嚣潜,道長(zhǎng),這世上最難降的妖魔是什么椅贱? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任懂算,我火速辦了婚禮,結(jié)果婚禮上庇麦,老公的妹妹穿的比我還像新娘计技。我一直安慰自己,他們只是感情好女器,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布酸役。 她就那樣靜靜地躺著,像睡著了一般驾胆。 火紅的嫁衣襯著肌膚如雪涣澡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天丧诺,我揣著相機(jī)與錄音入桂,去河邊找鬼。 笑死驳阎,一個(gè)胖子當(dāng)著我的面吹牛抗愁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播呵晚,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼蜘腌,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了饵隙?” 一聲冷哼從身側(cè)響起撮珠,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎金矛,沒想到半個(gè)月后芯急,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驶俊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年娶耍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饼酿。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡榕酒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奈应,我是刑警寧澤澜掩,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站杖挣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刚陡。R本人自食惡果不足惜惩妇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筐乳。 院中可真熱鬧歌殃,春花似錦、人聲如沸蝙云。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勃刨。三九已至波材,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間身隐,已是汗流浹背廷区。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贾铝,地道東北人隙轻。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像垢揩,于是被迫代替她去往敵國和親玖绿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348