深入理解CAS機(jī)制

逅弈 歡迎轉(zhuǎn)載丰捷,注明原創(chuàng)出處即可,謝謝寂汇!

要實(shí)現(xiàn)一個(gè)網(wǎng)站訪問量的計(jì)數(shù)器病往,可以通過一個(gè)Long類型的對(duì)象,并加上synchronized內(nèi)置鎖的方式骄瓣。但是這種方式使得多線程的訪問變成了串行的停巷,同一時(shí)刻只能有一個(gè)線程可以更改long的值,那么為了能夠使多線程并發(fā)的更新long的值榕栏,我們可以使用J.U.C包中的Atomic原子類畔勤。這些類的更新是原子的,不需要加鎖即可實(shí)現(xiàn)并發(fā)的更新扒磁,并且是線程安全的庆揪。

可是Atomic原子類是怎么保證并發(fā)更新的線程安全的呢?讓我們看一下AtomicLong的自增方法incrementAndGet():

public final long incrementAndGet() {
    // 無限循環(huán)妨托,即自旋
    for (;;) {
        // 獲取主內(nèi)存中的最新值
        long current = get();
        long next = current + 1;
        // 通過CAS原子更新缸榛,若能成功則返回,否則繼續(xù)自旋
        if (compareAndSet(current, next))
            return next;
    }
}
private volatile long value;
public final long get() {
    return value;
}

可以發(fā)現(xiàn)其內(nèi)部保持著一個(gè)volatile修飾的long變量兰伤,volatile保證了long的值更新后内颗,其他線程能立即獲得最新的值。
在incrementAndGet中首先是一個(gè)無限循環(huán)(自旋)敦腔,然后獲取long的最新值均澳,將long加1,然后通過compareAndSet()方法嘗試將long的值有current更新為next会烙。如果能更新成功负懦,則說明當(dāng)前還沒有其他線程更新該值,則返回next柏腻,如果更新失敗纸厉,則說明有其他線程提前更新了該值五嫂,則當(dāng)前線程繼續(xù)自旋嘗試更新。

CAS的基本思想是認(rèn)為當(dāng)前環(huán)境中的并發(fā)并沒有那么高沃缘,比較樂觀的看待整個(gè)并發(fā)槐臀,只需要在更新某個(gè)值時(shí)先檢查下該值有沒有發(fā)生變化水慨,如果沒有發(fā)生變化則更新敬扛,否則放棄更新朝抖。

CAS的操作其底層是通過調(diào)用sun.misc.Unsafe類中的CompareAndSwap的方法保證線程安全的。Unsafe類中主要有下面三種CompareAndSwap方法:

public final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);

public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);

可以看到這些方法都是native的急侥,需要調(diào)用JNI接口坏怪,也即通過操作系統(tǒng)來保證這些方法的執(zhí)行陕悬。

以上原子更新操作中除了CAS之外還有一個(gè)自旋(無限循環(huán))捉超,那么什么是自旋呢拼岳?為什么要用自旋呢?下面我們來了解一下自旋

自旋鎖

  • 簡(jiǎn)述

跟互斥鎖一樣惜纸,一個(gè)線程要想訪問被自旋鎖保護(hù)的共享資源耐版,必須先得到鎖粪牲,在訪問完共享資源后腺阳,必須釋放鎖亭引。

如果在獲取自旋鎖時(shí)焙蚓,沒有線程保持該鎖,那么將立即得到鎖赵哲;如果在獲取自旋鎖時(shí)鎖已經(jīng)有保持者,那么獲取鎖的操作將自旋在那里将宪,直到該自旋鎖的保持者釋放了鎖较坛。

  • 優(yōu)點(diǎn)

自旋鎖比較適用于鎖使用者保持鎖時(shí)間比較短的情況丑勤,比如執(zhí)行一個(gè)變量的自增操作法竞。

正是由于自旋鎖使用者一般保持鎖時(shí)間非常短,因此選擇自旋而不是睡眠是非常必要的薛躬,自旋鎖的效率遠(yuǎn)高于互斥鎖型宝,因?yàn)榫€程的睡眠趴酣、喚醒需要操作系統(tǒng)的支持岖寞,開銷比較大渊涝,因此當(dāng)一個(gè)操作保持鎖的時(shí)間非常短時(shí)跨释,不需要將線程掛起或睡眠,而是讓線程執(zhí)行一個(gè)忙循環(huán)岁疼,等到自旋鎖的持有者釋放了鎖之后瑰排,當(dāng)前線程將會(huì)獲得鎖暖侨。

  • 缺點(diǎn)

遞歸死鎖

試圖遞歸地獲得自旋鎖必然會(huì)引起死鎖:遞歸程序的持有實(shí)例在第二個(gè)實(shí)例循環(huán),以試圖獲得相同自旋鎖時(shí)京郑,不會(huì)釋放此自旋鎖

過多占用cpu資源

如果不加限制些举,由于申請(qǐng)者一直在循環(huán)等待户魏,因此自旋鎖在鎖定的時(shí)候,如果不成功,不會(huì)睡眠,會(huì)持續(xù)的嘗試叼丑。

單cpu的時(shí)候自旋鎖會(huì)讓其它process動(dòng)不了幢码。因此症副,一般自旋鎖實(shí)現(xiàn)會(huì)有一個(gè)參數(shù)限定最多持續(xù)嘗試次數(shù)政基,超出后, 自旋鎖放棄當(dāng)前time slice辕坝。 等下一次機(jī)會(huì)

雖然CAS可以高效的對(duì)某些共享變量進(jìn)行并發(fā)的更改酱畅,但是他也是有缺點(diǎn)的纺酸,其中之一就是ABA問題餐蔬。當(dāng)要更改的值從A變?yōu)锽樊诺,之后又變?yōu)锳词爬,則檢查時(shí)可能會(huì)發(fā)現(xiàn)沒有發(fā)生變化顿膨,實(shí)際上已經(jīng)發(fā)生了變化。解決方法是變更之前加上版本號(hào),如1A芽唇,2B取劫,3A炮捧∨乜危可通過AtomicStampedReference來解決ABA問題书蚪,這個(gè)類的compareAndSet方法殊校,將首先檢查當(dāng)前引用是否等于預(yù)期引用为流,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志敬察,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值虫给,否則不予更新。

除此之外缠黍,在并發(fā)量非常高的情況下瓷式,CAS失敗的幾率將變得非常高语泽,重試的次數(shù)也會(huì)跟著增加贸典,越多線程重試,CAS失敗的幾率就越高踱卵,變成惡性循環(huán)廊驼。因此在并發(fā)量非常高的環(huán)境中,如果仍然想通過原子類來更新的話惋砂,可以使用AtomicLong的替代類:LongAdder妒挎。

將單一value的更新壓力分擔(dān)到多個(gè)value中去,降低單個(gè)value的“熱度”西饵,分段更新酝掩,這樣,線程數(shù)再多也會(huì)分擔(dān)到多個(gè)value上去更新眷柔,只需要增加value的個(gè)數(shù)就可以降低value的 “熱度”,這樣AtomicLong中的惡性循環(huán)就可以解決了。

在LongAdder中cells就是這個(gè)“段”,cell中的value就是存放更新值的,這樣,當(dāng)我需要總數(shù)時(shí)牛郑,把cell中的value都累加一下不就可以了么

讓我們看一下LongAdder更新的原則:

1.當(dāng)并發(fā)低時(shí)先采用CAS進(jìn)行更新础芍,如果更新成功即返回
2.當(dāng)并發(fā)高且CAS更新失敗時(shí),則進(jìn)入分段更新

LongAdder的部分代碼實(shí)現(xiàn):

/**
 * Adds the given value.
 *
 * @param x the value to add
 */
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 當(dāng)并發(fā)低時(shí)先采用CAS進(jìn)行add,如果更新成功即返回
    // 當(dāng)并發(fā)高且CAS更新失敗時(shí),則進(jìn)入分段更新
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            // 找到cells數(shù)組中該值對(duì)應(yīng)的cell對(duì)象
            (a = as[getProbe() & m]) == null ||
            // 使用cell對(duì)象的cas方法進(jìn)行更新
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

/**
 * Handles cases of updates involving initialization, resizing,
 * creating new Cells, and/or contention. See above for
 * explanation. This method suffers the usual non-modularity
 * problems of optimistic retry code, relying on rechecked sets of
 * reads.
 *
 * @param x the value
 * @param fn the update function, or null for add (this convention
 * avoids the need for an extra field or function in LongAdder).
 * @param wasUncontended false if CAS failed before call
 */
final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        if ((as = cells) != null && (n = as.length) > 0) {
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {       // Try to attach new Cell
                    Cell r = new Cell(x);   // Optimistically create
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) {      // Expand table unless stale
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = advanceProbe(h);
        }
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

需要注意的是朦佩,雖然AtomicLong等原子類的更新是原子的弄砍,但是多個(gè)原子操作合并后的操作卻不是原子的衣式,也即:原子+原子!=原子乃正,下面將用一個(gè)例子來說明該問題:

private CountDownLatch latch;

private class Discovery{

    private Map<String,SlaveNode> slaveNodeMap;

    private AtomicInteger slaveIndex;

    public Discovery(){
        slaveNodeMap = new HashMap<String,SlaveNode>();
        SlaveNode slaveNode1 = new SlaveNode("127.0.0.1",8081);
        SlaveNode slaveNode2 = new SlaveNode("127.0.0.1",8082);
        slaveNodeMap.put(slaveNode1.getId(),slaveNode1);
        slaveNodeMap.put(slaveNode2.getId(),slaveNode2);
        slaveIndex = new AtomicInteger(0);
    }

    public SlaveNode discover() {
        if (slaveNodeMap.size() == 0) {
            System.err.println("No available SlaveNode!");
            return null;
        }
        SlaveNode[] nodes = new SlaveNode[]{};
        nodes = slaveNodeMap.values().toArray(nodes);
        // 通過CAS循環(huán)獲取下一個(gè)可用服務(wù)
        // 當(dāng)當(dāng)前索引為數(shù)組的長(zhǎng)度是划栓,將索引值更新為0
        slaveIndex.compareAndSet(nodes.length,0);
        System.out.println("currentIndex=" + slaveIndex + ",currentThread=" + Thread.currentThread().getName());
        // 根據(jù)數(shù)組的下標(biāo)獲取可用的服務(wù)委煤,之后將索引通過原子方式加1
        return nodes[slaveIndex.getAndIncrement()];
    }

}

@Test
public void testConcurrentDiscover(){
    int loopTimes = 300;

    latch = new CountDownLatch(loopTimes);
    Discovery discovery = new Discovery();
    class Runner implements Runnable{
        @Override
        public void run() {
            Object object = discovery.discover();
            System.out.println(String.format("object={%s},currentThread={%s}",(object!=null?object.toString():"null"),Thread.currentThread().getName()));
            latch.countDown();
        }
    }
    for(int i=0;i<loopTimes;i++){
        new Thread(new Runner()).start();
    }
    try {
        latch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

該方法執(zhí)行的可能會(huì)成功,即多線程交替獲得SlaveNode,但也會(huì)出現(xiàn)錯(cuò)誤,slaveIndex的值會(huì)超過數(shù)組的長(zhǎng)度赏淌,問題就出在這段代碼:

slaveIndex.compareAndSet(nodes.length,0);
return nodes[slaveIndex.getAndIncrement()];

這兩個(gè)操作本身都是原子的,但是合并在一起就不是原子的了场靴,因此會(huì)出現(xiàn)錯(cuò)誤,解決的方法還是對(duì)整個(gè)執(zhí)行的過程加鎖哮兰。

更多原創(chuàng)好文,請(qǐng)關(guān)注「逅弈逐碼」
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市劝篷,隨后出現(xiàn)的幾起案子只估,更是在濱河造成了極大的恐慌,老刑警劉巖彬向,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幢泼,死亡現(xiàn)場(chǎng)離奇詭異招驴,居然都是意外死亡触趴,警方通過查閱死者的電腦和手機(jī)眯娱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門考廉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人携御,你說我怎么就攤上這事昌粤。” “怎么了啄刹?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵涮坐,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我誓军,道長(zhǎng)袱讹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任昵时,我火速辦了婚禮捷雕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘壹甥。我一直安慰自己救巷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布句柠。 她就那樣靜靜地躺著浦译,像睡著了一般。 火紅的嫁衣襯著肌膚如雪溯职。 梳的紋絲不亂的頭發(fā)上精盅,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音谜酒,去河邊找鬼叹俏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛甚带,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播佳头,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼鹰贵,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了康嘉?” 一聲冷哼從身側(cè)響起碉输,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亭珍,沒想到半個(gè)月后敷钾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枝哄,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年阻荒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挠锥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡侨赡,死狀恐怖蓖租,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情羊壹,我是刑警寧澤蓖宦,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站油猫,受9級(jí)特大地震影響稠茂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜情妖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一睬关、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鲫售,春花似錦共螺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至秦效,卻和暖如春雏蛮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阱州。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工挑秉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苔货。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓犀概,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親夜惭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姻灶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容