異步無鎖化編程

1. 問題

1.1鎖的問題

要實現(xiàn)并發(fā),最常見的方式是加鎖沈矿。在使用鎖實現(xiàn)并發(fā)時上真,線程的掛起、恢復過程中存在很大的開銷细睡。如果競爭比較激烈谷羞,調(diào)度開銷和實際工作開銷的比值會很高。

此外,當一個線程正在等待鎖時葛超,它不能做任何事逆粹。如果一個線程在持有鎖的情況下被延遲執(zhí)行,那么所有需要這個鎖的線程都無法執(zhí)行下去猫妙。如果被阻塞的線程優(yōu)先級高,而持有鎖的線程優(yōu)先級低,將會導致優(yōu)先級反轉(zhuǎn)(Priority Inversion)图贸。即 高優(yōu)先級的線程即使可以搶先執(zhí)行,也需要等待鎖被釋放冕广,在這種情況下疏日,高優(yōu)先級線程的級別實際上就降到了低優(yōu)先級線程的級別。

1.2 volatile的問題

與鎖相比撒汉,volatile變量是一種更輕量級的同步機制沟优,因為在使用這些變量時不會發(fā)生上下文切換和線程調(diào)度等操作,可以保證線程之間的可見性和讀寫的原子性睬辐。

但是volatile變量也有其局限:不能用于構建原子的復合操作挠阁,因此當一個變量依賴其他變量或者舊值時就不能使用volatile變量。我們無法用volatile實現(xiàn)原子的更新操作溯饵。

基于上述問題侵俗,我們期望能有一種開銷更低、更安全并且支持原子的更新操作的機制丰刊。

2. 樂觀鎖與悲觀鎖

獨占鎖是一種悲觀鎖隘谣,synchronized就是一種獨占鎖,它假設最壞的情況啄巧,并且只有在確保其它線程不會造成干擾的情況下執(zhí)行寻歧,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖棵帽。而另一個更加有效的鎖就是樂觀鎖熄求。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作逗概,如果因為沖突失敗就重試弟晚,直到成功為止。

3. CAS

3.1 CAS介紹

大多數(shù)處理器都提供了一種CAS(Compare and swap,比較并交換)指令卿城,CAS有3個操作數(shù)枚钓,內(nèi)存值V,舊的預期值A瑟押,要寫入的新值B搀捷。CAS的含義是:當且僅當預期值A和內(nèi)存值V相同時,將內(nèi)存值V修改為B多望,否則什么都不做嫩舟。無論V的值是否等于A,都返回V原有的值怀偷。 CAS原理圖如下家厌。

可以將CAS近似理解為一種樂觀鎖的技術。 當多個線程嘗試使用CAS同時更新同一個變量時椎工,只有其中一個線程能更新變量的值饭于,而其它線程都失敗,失敗的線程并不會被掛起维蒙,而是被告知在這次競爭中失敗了掰吕,可以再次嘗試。 由于線程在競爭失敗時不會被掛起颅痊,所以線程可以靈活地決定在失敗時執(zhí)行什么操作(重試殖熟、恢復操作或什么都不做)。

下面這段代碼來自《Java并發(fā)編程實戰(zhàn)》八千,幫助我們更好地理解CAS的語義:

public synchronized int compareAndSwap(int expectValue, int newValue) {
        int old = this.value;
        if (old == expectValue) {
            this.value = newValue;
        }
        return old;
}

public synchronized boolean compareAndSet(int expectValue, int newValue) {
  return (expectValue == compareAndSwap(expectValue, newValue));
}

3.2 CAS的優(yōu)勢:

CAS是CPU指令級的操作吗讶,只有一步原子操作,避免了在JVM級和操作系統(tǒng)級的鎖定恋捆、線程掛起和上下文切換照皆,所以性能比加鎖更快。這里可以展開很多沸停,暫時不詳述膜毁。

4. JVM對CAS的支持

在Java的原子類變量中,如java.util.concurrent.atomic中的AtomicXXX愤钾,都使用了這些底層的JVM支持為數(shù)字類型的引用類型提供一種高效的CAS操作瘟滨,而在java.util.concurrent中的大多數(shù)類在實現(xiàn)時都直接或間接地使用了這些原子變量類。

4.1 Atomic變量

以AtomicInteger變量為例來說明能颁。AtomicInteger中的部分方法實現(xiàn):

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

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

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

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

可以看到杂瘸,在實現(xiàn)中,都調(diào)用了sun.misc.Unsafe類庫里面的CAS算法伙菊,用處理器提供的CAS指令來實現(xiàn)無鎖自增败玉。

4.2 非阻塞算法

如果在某種算法中敌土,一個線程的失敗或掛起不會導致其他線程也失敗或掛起,那么這種算法就稱為非阻塞算法运翼。

如果在算法的每個步驟中都存在某個線程能夠執(zhí)行下去返干,那么這種算法也被稱為無鎖算法。

如果在算法中僅將CAS用于協(xié)調(diào)線程之間的操作血淌,并且正確地實現(xiàn)矩欠,那么它既是一種無阻塞算法,又是一種無鎖算法悠夯。

4.3 無鎖棧

下面是一個無鎖的非阻塞算法的例子癌淮,無鎖棧ConcurrentStack。

在ConcurrentStack中沦补,push() 方法觀察當前棧頂?shù)墓?jié)點该默,構建一個新節(jié)點放在堆棧上,然后策彤,如果棧頂?shù)墓?jié)點在初始觀察之后沒有變化,那么就安裝新節(jié)點匣摘。如果 CAS 失敗店诗,意味著另一個線程已經(jīng)修改了堆棧,那么過程就會重新開始音榜。

類似地庞瘸,在pop()方法中,先獲取棧頂?shù)墓?jié)點赠叼,再根據(jù)棧頂?shù)墓?jié)點獲取第二個節(jié)點(即新的棧頂節(jié)點)擦囊,如果棧頂?shù)墓?jié)點在初始觀察之后沒有變化,那么就安裝新節(jié)點嘴办。如果 CAS 失敗瞬场,意味著另一個線程已經(jīng)修改了堆棧,那么過程就會重新開始涧郊。

public class ConcurrentStack<E> {
    AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
    
    public void push(E item) {
        Node<E> newHead = new Node<E>(item);
        Node<E> oldHead;
        while (true) {
            oldHead = top.get();
            newHead.next = oldHead;
            if (top.compareAndSet(oldHead, newHead)) {
                return;
            }
        }
    }
    
    public E pop() {
        while (true) {
            Node<E> oldHead = top.get();
            if (oldHead == null) {
                return null;
            }
            Node<E> newHead = oldHead.next;
            if (top.compareAndSet(oldHead, newHead)) {
                return oldHead.item;
            }
        }
    }
    
    private static class Node<E> {
        public final E item;
        public Node<E> next;
        
        public Node(E item) {
            this.item = item;
        }
    }
}

5.ABA問題

這是一種異常情況:當線程獲得對象當前數(shù)據(jù)后贯被,在準備修改為新值前,對象的值被其他線程連續(xù)修改了兩次妆艘,而經(jīng)過這兩次修改后彤灶,對象的值又恢復為舊值。即由A變?yōu)锽批旺,又變?yōu)锳(ABA)幌陕。在一些算法中,對象的值如果經(jīng)過了A-B-A的變化汽煮,仍然會被認為發(fā)生了變化搏熄,需要重新執(zhí)行算法中的某些步驟棚唆。

針對這種情況,有一種常見的解決方案:不是更新某個值搬卒,而是更新值和版本號瑟俭。即使某個值由A變?yōu)锽又變回A,版本號也是不同的契邀。 AtomicStampedReference 和 AtomicMarkableReference 支持在兩個變量上執(zhí)行原子的條件更新摆寄。AtomicStampedReference可以原子地更新 ”對象-引用“ 二元組,通過增加版本號坯门,避免ABA問題微饥。 AtomicMarkableReference可以原子地更新 ”對象-布爾值“ 二元組,通過增加標記值古戴,實現(xiàn)特殊的標記功能欠橘。一個例子是:通過AtomicMarkableReference二元組使節(jié)點保存在鏈表中同時又將其標記為”已刪除的節(jié)點“。

6.總結(jié)

鎖是常見的實現(xiàn)并發(fā)的方式现恼,但有很多局限性肃续。處理器的CAS指令實現(xiàn)了原子級別的更新操作,通過比較并交換而不是鎖來維持線程的安全性叉袍,實現(xiàn)并發(fā)始锚。在JAVA中,這些底層的指令通過原子變量類向外公開喳逛,為整數(shù)和對象引用提供原子的更新操作瞧捌。

基于這些原子的更新操作,我們可以設計實現(xiàn)非阻塞的算法和無鎖的算法润文。這類算法通常能提供更高的可伸縮性姐呐,并能更好地防止活躍性故障。 這類功能也可以為我們在實際工作中設計并發(fā)算法提供思路典蝌。

參考文檔:

非阻塞同步算法與CAS(Compare and Swap)無鎖算法
https://blog.csdn.net/fjslovejhl/article/details/17025001
https://cloud.tencent.com/developer/article/1362763
《Java并發(fā)編程實戰(zhàn)》
sun.misc.Unsafe詳解和CAS底層實現(xiàn)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末曙砂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赠法,更是在濱河造成了極大的恐慌麦轰,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砖织,死亡現(xiàn)場離奇詭異款侵,居然都是意外死亡,警方通過查閱死者的電腦和手機侧纯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門新锈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人眶熬,你說我怎么就攤上這事妹笆】榍耄” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵拳缠,是天一觀的道長墩新。 經(jīng)常有香客問我,道長窟坐,這世上最難降的妖魔是什么海渊? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮哲鸳,結(jié)果婚禮上臣疑,老公的妹妹穿的比我還像新娘。我一直安慰自己徙菠,他們只是感情好讯沈,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婿奔,像睡著了一般缺狠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萍摊,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天儒老,我揣著相機與錄音,去河邊找鬼记餐。 笑死,一個胖子當著我的面吹牛薇正,可吹牛的內(nèi)容都是我干的片酝。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼挖腰,長吁一口氣:“原來是場噩夢啊……” “哼雕沿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起猴仑,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤审轮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辽俗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疾渣,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年崖飘,在試婚紗的時候發(fā)現(xiàn)自己被綠了榴捡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡朱浴,死狀恐怖吊圾,靈堂內(nèi)的尸體忽然破棺而出达椰,到底是詐尸還是另有隱情,我是刑警寧澤项乒,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布啰劲,位于F島的核電站,受9級特大地震影響檀何,放射性物質(zhì)發(fā)生泄漏蝇裤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一埃碱、第九天 我趴在偏房一處隱蔽的房頂上張望猖辫。 院中可真熱鬧,春花似錦砚殿、人聲如沸啃憎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辛萍。三九已至,卻和暖如春羡藐,著一層夾襖步出監(jiān)牢的瞬間贩毕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工仆嗦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辉阶,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓瘩扼,卻偏偏與公主長得像谆甜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子集绰,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

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