鎖與無鎖的雜談

先定一個全文的基調(diào),本文只是自己對鎖相關(guān)東西的一點(diǎn)想法,寫下來整理下,前文寫了點(diǎn)對鎖的理解,后文貼了點(diǎn)自己嘗試的無鎖數(shù)據(jù)結(jié)構(gòu),不是特別嚴(yán)謹(jǐn),只是我的一點(diǎn)嘗試和探索。
just my humble opinion, for reference only
在并發(fā)程序中,保證訪問的有序性是非常重要的,內(nèi)存模型也規(guī)范了代碼來訪問共享變量的順序,本文不太涉及虛擬機(jī)的一些實(shí)現(xiàn),只是在應(yīng)用層面分析下各自的有點(diǎn)與取舍。

在Java中,最簡單的保證訪問順序的就是用鎖了

Synchronized關(guān)鍵字


Synchronized這個關(guān)鍵字使用是最簡單的,可以加在普通方法名上相當(dāng)于用this來加鎖,用在靜態(tài)方法名上相當(dāng)于this.class來加鎖,也可以單獨(dú)加在代碼塊上自己制定某個對象來加鎖,這樣的例子網(wǎng)上數(shù)不勝數(shù),我也不就在這多展開了,底層實(shí)現(xiàn)是虛擬機(jī)在字節(jié)碼上價了個montior,拋開底層,從應(yīng)用層講的話,就是對對象的對象頭通過CAS重寫,簡單說下這個過程,就是每個對象都有一部分叫做“Mark Word”的空間,一般是32位,上面有著對象的類信息指針、年齡(用于GC)、hash值、以及相關(guān)鎖的信息,synchronized關(guān)鍵字其實(shí)就是每個線程走到這后去改對象頭里面鎖的信息,改成自己的了就是獲取到鎖了,改幾次都改失敗了,那么就得等著掛起了,這里我只是粗略的描述了下過程,大家可以網(wǎng)上搜搜synchronized底層實(shí)現(xiàn),包括JVM優(yōu)化的偏向鎖輕量級鎖等,都值得深究。

網(wǎng)上找的圖,網(wǎng)上這樣的圖很多,可以自己好好研究下

顯示鎖


Java中的顯示鎖最多的就是ReentrantLock了,在我的上一篇文章中分析AQS時花了很多筆墨分析其實(shí)現(xiàn)與特性,有問題的讀者可以點(diǎn)鏈接閱讀我前一篇博文。

Synchronized VS ReentrantLock


對于這兩者的比較,相同點(diǎn)很簡單,都是為了保證對于某些非線程安全的區(qū)域的順序訪問,不同點(diǎn)的話,我從功能和實(shí)現(xiàn)去比較下兩者

  • 功能
    synchronized這個關(guān)鍵字呢,這樣說,就是他一旦開始等這個鎖,他就會認(rèn)為自己有朝一日定能拿到執(zhí)行權(quán),無比自信,然而ReentrantLock就不這樣,他并不那么堅(jiān)定的認(rèn)為自己一定能拿到鎖,他會試探,就有了tryLock方法,他會給自己留退路,就有了lockInterruptly方法,他不像synchronizd那樣執(zhí)拗,所以tryLock方法還有帶超時機(jī)制的版本。除此之外,ReetrantLock還有綁定多個Condition功能,以及公平非公平模式,其實(shí)還有些api,可以看看源碼里面返回boolean值的方法,在這不多說啦,這就是他們在功能上的差異。

  • 實(shí)現(xiàn)
    在文章開頭已經(jīng)說到了這兩者實(shí)現(xiàn)的差異,一個是由JVM去實(shí)現(xiàn),加個monitor保證訪問時的排他性,一個是基于AQS實(shí)現(xiàn)的排他類工具,也因?yàn)閮烧叱錾牟煌?,synchronized未來有更多的優(yōu)化余地,現(xiàn)在兩者性能上幾乎也沒差距,官方建議是除非你需要ReentrantLock的功能,不然就用synchronized關(guān)鍵字,最后選哪個還是看你自己咯。

Lock-Free 無鎖


說完鎖,說說無鎖把,無鎖的核心就是CompareAndSet(CAS)了,并發(fā)包里面以CAS為核心實(shí)現(xiàn)的工具類也特別多,可以說整個Atomic包都是把,但是CAS算法也不是萬能的,在極大的并發(fā)程度下性能是不如鎖的,這也很好理解,一群人占著僅有的資源重復(fù)做一些事,這些事又注定只有少部分人能成功,大部分的人效率自然低,Atomic包中的AtomicLong就是以CAS去實(shí)現(xiàn)加減操作,但是新版本的JDK中又多了LongAdder類,這是為什么?原因就是為了盡量避免前面提到的那種“大部分人都失敗少部分人成功”的現(xiàn)象,其思路就是分散熱點(diǎn)域,比如a要加1,AtomicLong就是先取出a的值,再用a+1的值去覆蓋,不斷嘗試直至成功,LongAdder的思路就是把a(bǔ) 分為多個數(shù)值,比如a-3, 2 ,1三個部分,然后選中一部分,做AtomicLong做的事,最后全部累加起來,但是因?yàn)閍被分為多個部分,多個線程執(zhí)行操作時可以很好的分散開來,不會集中在一個地方不斷嘗試,這樣做正確性也是毋庸置疑的,畢竟整體+1和部分+1肯定是相等的。

Atomic包有很多這樣的類,都可以研究研究,而對于無鎖數(shù)據(jù)結(jié)構(gòu)的編寫,那就是一些experts要研究的東西了,在這里我寫兩個玩具demo,讓大家體會下。

Lock Free Queue

無鎖的隊(duì)列自己寫起來也不是太難,因?yàn)槌鲫?duì)入隊(duì)只在頭尾節(jié)點(diǎn),所以只要處理下頭尾節(jié)點(diǎn)就可以,無鎖隊(duì)列的問題在于我在尾節(jié)點(diǎn)插入時,可能此時此刻有同樣的操作在另一個線程進(jìn)行,所以我拿到的尾節(jié)點(diǎn)可能是過期的,所以要通過CAS嘗試。
我們拿尾節(jié)點(diǎn)插入來展示,頭節(jié)點(diǎn)讀者可以對照實(shí)現(xiàn)

public class LockFreeQueue<V> {
        private class Node<V> {//內(nèi)部節(jié)點(diǎn)
             V value = null;
             AtomicReference<Node<V>> next = null;
             Node(V value, Node next) {
                this.value = value;
                this.next = new AtomicReference<>(next);
            }
        }
        private AtomicReference<Node<V>> head = null;                       
        private AtomicReference<Node<V>> tail = null;  
        public LockFreeQueue(){
            Node<V> dummy = new Node<V>(null,null);                                  
            head = new AtomicReference<>(dummy);
            tail = new AtomicReference<>(dummy);
        }
        public boolean enQueue(V value) {
            Node<V> newNode = new Node<V>(value,null);
            boolean success=false;
            while(!success){//插入的時候可能有其他節(jié)點(diǎn)搶先入隊(duì),tail會失效
                Node<V> oldTail = tail.get();
                AtomicReference<Node<V>> nextNode = oldTail.next;
                success=nextNode.compareAndSet(null,newNode);
                if(success) return true;
            }
            return false;
        }
}

Lock Free List
隊(duì)列還是挺好理解的,下面我貼下我寫了大半天的無鎖單鏈表,不保證可伸縮性,簡單測試了下,因?yàn)閱捂湵聿迦霑羞@樣的問題,比如A->B->C->D的鏈表,我想在BC間插一個X,當(dāng)我拿到B時,按理說我應(yīng)該連成B->X->C這樣的,但是有可能我拿到B的時候,C被人刪了,但是我不知道,我還是按B->X->C連的話,鏈表就被我弄斷了,同理,如果此時此刻B被人刪了,還這么連,鏈表一樣會斷,同理的同理,如果這時候有人在BC中已經(jīng)差了個Y,鏈表已經(jīng)是B->Y->C了,我還按B->X->C連,Y就丟了,所以要顧及的情況很多,remove方法也有如上所述的問題,網(wǎng)上對于無鎖鏈表的實(shí)現(xiàn)不多,我是參考這篇論文實(shí)現(xiàn)的,這篇論文將刪除分為兩步,即先標(biāo)記(邏輯刪除),再實(shí)際刪除,我找了下JDK的工具類,認(rèn)為AtomicMarkableReference這個類。這個類內(nèi)部有個Pair(和C++中的pair差不多)就是一個二元組,第一個是持有的對象,第二個boolean標(biāo)記值,符合要求,下面我貼一下代碼。

public class ConcurrentLinkedList<T> {
    private class Node<T> {
        public Node(T value, AtomicMarkableReference<Node<T>> next) {
            this.value = value;
            this.next = next;
        }
        T value;
        AtomicMarkableReference<Node<T>> next;
    }
    AtomicMarkableReference<Node<T>> head
            = new AtomicMarkableReference<>(new Node<>(null, null), false);
    //其實(shí)這里還有邏輯分支要處理,就是尾端插入時node.next.isMarked()會
    //有null異常,但因?yàn)橹R演示核心思路,所以就略去了(因?yàn)樗接邢蓿?    //代碼會寫的很丑,有心的讀者可以自己實(shí)現(xiàn),讓我學(xué)習(xí)學(xué)習(xí))
    public boolean insert(T after, T value) {
        boolean success = false;
        while (!success) {
            for (Node<T> node = head.getReference(); node != null && !node.next.isMarked(); node = node.next.getReference()) {
                if (node.value.equals(after) && !node.next.isMarked()) {
                    Node<T> nextNode = node.next.getReference();
                    Node<T> insertNode = new Node<>(value, new AtomicMarkableReference<>(nextNode, false));
                    success = node.next.compareAndSet(nextNode, insertNode, false, false);
                    if (success) return true;
                } else if (node.next == null) {//如果已經(jīng)是最后一個結(jié)點(diǎn),還沒有匹配到就返回false
                    return false;
                }
            }
        }
        return false;
    }

    public boolean remove(T aim) {
        boolean success = false;
        while (!success) {
            for (Node<T> predecessor = head.getReference(); predecessor != null && !predecessor.next.isMarked();
                 predecessor = predecessor.next.getReference()) {
                AtomicMarkableReference<Node<T>> target = predecessor.next;   //要刪除的目標(biāo)節(jié)點(diǎn)
                AtomicMarkableReference<Node<T>> successor = target.getReference().next;//目標(biāo)節(jié)點(diǎn)的后繼結(jié)點(diǎn)
                if (target.getReference().equals(aim) && !successor.isMarked()) {
                    while (!target.attemptMark(target.getReference(), true)) {}//標(biāo)記已經(jīng)刪除,即邏輯刪除
                    success = predecessor.next.compareAndSet(target.getReference(), successor.getReference(), true, false);//這里是物理刪除
                    if (success) return true;
                } else if (successor == null)
                    return false;//如果目標(biāo)節(jié)點(diǎn)的后繼結(jié)點(diǎn)已經(jīng)是null了,說明已經(jīng)到鏈表結(jié)尾了,
            }
        }
        return false;
    }
}

我自己實(shí)現(xiàn)的代碼已經(jīng)貼出來了,就當(dāng)成偽代碼看吧!自己也花了很多時間,我手動構(gòu)造了點(diǎn)數(shù)據(jù)沒出現(xiàn)什么問題,但是我不知道更加復(fù)雜的場景會不會出現(xiàn)問題,這只是我給出的思路,not solution,just my opinion,網(wǎng)上我也沒找到很好的關(guān)于無鎖鏈表的實(shí)現(xiàn),只有一些論文,給出了思路,當(dāng)然思路和實(shí)現(xiàn)細(xì)節(jié)還是有差距,我貼出了我的看法,如果有讀者有更好的實(shí)現(xiàn)希望能放出來也讓我學(xué)習(xí)學(xué)習(xí)。

我寫這篇文章不是希望用這么點(diǎn)篇幅就能把很多鎖和無鎖的細(xì)節(jié)都描述的淋漓盡致甚至庖丁解牛,我更多的目的只是希望能作為一篇“雜談”談?wù)勎覍o鎖和鎖的看法,給讀者提供一些我的理解,幫助讀者在自己的“深究之路上”提供更多的墊腳石,可能很多細(xì)節(jié)還是比較粗糙,歡迎吐槽。

最后再聲明下Just my humble opinion,I'm open to discussion
參考論文:(那個插圖我很早保存的,實(shí)在記不起哪里來的了,如有侵權(quán),望通知,必刪)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評論 2 372

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

  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,720評論 0 11
  • 一個簡單的單例示例 單例模式可能是大家經(jīng)常接觸和使用的一個設(shè)計(jì)模式,你可能會這么寫 publicclassUnsa...
    Martin說閱讀 2,253評論 0 6
  • 前言 上一篇文章《基于CAS操作的Java非阻塞同步機(jī)制》 分析了非同步阻塞機(jī)制的實(shí)現(xiàn)原理,本篇將分析一種以非同步...
    Mars_M閱讀 4,822評論 5 9
  • 非內(nèi)置鎖存在的意義 synchronized關(guān)鍵字提供了一套非常完整的java內(nèi)置鎖實(shí)現(xiàn),簡單易用通過塊語句控制鎖...
    wiizhang閱讀 495評論 0 0
  • 在這個混亂的社會,沒什么東西是正常的。人也一樣,亂言亂語的述說著,沒人理會,只當(dāng)你是神經(jīng)病罷了。
    瘞心閱讀 258評論 0 0