關(guān)于編程路上的一些雜談 多線程中鎖的秘密(二)

來源:極樂科技知乎專欄
作者:知秋
博客:一葉知秋


接上篇關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一),其實已經(jīng)討論一些鎖的實現(xiàn)了狂男,這里再深入一下,把問題講明白。

底層實現(xiàn)原理

有volatile變量修飾的共享變量進行寫操作的時候會多出第二行匯編代碼煮仇,通過查IA-32架構(gòu)軟件開發(fā)者手冊可知,Lock前綴的指令在多核處理器下會引發(fā)了兩件事情谎仲。

  1. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存浙垫。

  2. 這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。

為了提高處理速度郑诺,處理器不直接和內(nèi)存進行通信绞呈,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進行操作间景,但操作完不知道何時會寫到內(nèi)存佃声。如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令倘要,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存圾亏。但是,就算寫回到內(nèi)存封拧,如果其他處理器緩存的值還是舊的志鹃,再執(zhí)行計算操作就會有問題。所以泽西,在多處理器下曹铃,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議捧杉,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了陕见,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改秘血,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器對這個數(shù)據(jù)進行修改操作的時候评甜,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里灰粮。

同樣,參照上面所說的忍坷,對于volatile來說粘舟,它的實現(xiàn)也不外乎需要達(dá)到以下兩種實現(xiàn)效果:

1)Lock前綴指令會引起處理器緩存回寫到內(nèi)存Lock前綴指令會引起處理器緩存回寫到內(nèi)存

2)一個處理器的緩存回寫到內(nèi)存會導(dǎo)致其他處理器的緩存無效

對象頭

對象頭:包括兩部分信息。第一部分用于存儲對象自身的運行時數(shù)據(jù)佩研,如哈希碼柑肴,GC分代年齡、鎖狀態(tài)旬薯、線程持有鎖嘉抒、等等。這部分?jǐn)?shù)據(jù)的長度在32為或64位袍暴,官方稱之為“MarkWord”些侍。對象頭的另一部分是類型指針,即對象指向它的類元素的指針政模,通過這個指針來確定這個對象時那個類的實例岗宣。(如果Java對象時一個數(shù)組,則對象頭還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)淋样。因為Java數(shù)組元數(shù)據(jù)中沒有數(shù)組大小的記錄)

偏向鎖的概念

HotSpot的作者經(jīng)過研究發(fā)現(xiàn)耗式,大多數(shù)情況下,鎖不僅不存在多線程競爭趁猴,而且總是由同一線程多次獲得刊咳,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當(dāng)一個線程訪問同步塊并獲取鎖時儡司,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID娱挨,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖捕犬。如果測試成功跷坝,表示線程已經(jīng)獲得了鎖。如果測試失敗碉碉,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置柴钻,則使用CAS競爭鎖;如果設(shè)置了垢粮,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程贴届。

進入正題

關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一)其實已經(jīng)有講到volatile 的實現(xiàn)方式的,通過上面的深入想必已經(jīng)有更細(xì)致的了解,然后也相信大家對于像i++ 這種復(fù)合操作不具有原子性(i是volatile變量 )很是疑惑毫蚓,這里要說一個概念:

程序計數(shù)器PC

程序計數(shù)器即指令地址寄存器占键。在某些計算機中用來存放當(dāng)前正在執(zhí)行的指令地址;而在另一些計算機中則用來存放即將要執(zhí)行的下一條指令地址绍些;而在有指令預(yù)取功能的計算機中捞慌,一般還要增加一個程序計數(shù)器用來存放下一條要取出的指令地址耀鸦。程序計數(shù)器用以指出下條指令在主存中的存放地址柬批,CPU根據(jù)PC的內(nèi)容去主存取得指令。因程序中指令是順序執(zhí)行的袖订,所以PC有自增功能氮帐。

也就是說其實i++可以理解成一條指令,而i=i+1便是兩條指令了包括i+1和將結(jié)果賦給i洛姑,應(yīng)該不需要我再深入了上沐,已經(jīng)很明了了。

鎖的語義

這里在關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一)已經(jīng)有說其底層還是依靠volatile來實現(xiàn)楞艾,接下來就通過ReentrantLock源碼來具體對其進行分析:


對于compareAndSetState來說:

CAS, CPU指令参咙,在大多數(shù)處理器架構(gòu),包括IA32硫眯、Space中采用的都是CAS指令蕴侧,CAS的語義是“我認(rèn)為V的值應(yīng)該為A,如果是两入,那么將V的值更新為B净宵,否則不修改并告訴V的值實際為多少”,CAS是項 樂觀鎖 技術(shù)裹纳,當(dāng)多個線程嘗試使用CAS同時更新同一個變量時择葡,只有其中一個線程能更新變量的值,而其它線程都失敗剃氧,失敗的線程并不會被掛起敏储,而是被告知這次競爭中失敗,并可以再次嘗試朋鞍。

CAS有3個操作數(shù)虹曙,內(nèi)存值V,舊的預(yù)期值A(chǔ)番舆,要修改的新值B酝碳。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B恨狈,否則什么都不做疏哗。

對于compareAndSetState來說:它是個原子方法,原理就是是CAS.這個是高效,而且是原子的,不用加鎖. 也會因為其他值改了而產(chǎn)生誤操作,應(yīng)為會先判斷當(dāng)前值,符合期望才去改變,而我們所要操作的值無非就是state而已。

對于上面截圖的代碼說的直白點就是對于一個線程如果當(dāng)前沒有競爭禾怠,則直接拿到或者上鎖返奉,否則贝搁,嘗試獲取即acquire(1)方法:

/**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
/**
    * Acquires in exclusive mode, ignoring interrupts.  Implemented
    * by invoking at least once {@link #tryAcquire},
    * returning on success.  Otherwise the thread is queued, possibly
    * repeatedly blocking and unblocking, invoking {@link
    * #tryAcquire} until success.  This method can be used
    * to implement method {@link Lock#lock}.
    *
    * @param arg the acquire argument.  This value is conveyed to
    *        {@link #tryAcquire} but is otherwise uninterpreted and
    *        can represent anything you like.
    */
   public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }

首先通過tryAcquire()方法嘗試獲取,如果不能的話芽偏,則通過AddWaiter()方法雷逆,用當(dāng)前線程生成一個Node放入隊尾,而acquireQueued()則是一種自旋鎖的實現(xiàn)方式污尉。最后把當(dāng)前線程interrupt膀哲。這里可以發(fā)現(xiàn),java的 AQS的實現(xiàn)很巧妙的一個地方就是把tryAcquire延遲到子類去實現(xiàn)被碗。公平鎖和非公平鎖的實現(xiàn)方式是不一樣的某宪。非公平鎖的tryAcquire()的是通過nonfairTryAcquire()。

然后看acquireQueued(),其實就是一個無限循環(huán)锐朴,直到獲得鎖為止兴喂。通過上圖源碼可以看到在shouldParkAfterFailedAcquire()方法中,通過前一個Node的waitStatus來判斷是否應(yīng)該把當(dāng)前線程阻塞(所以用了雙&&開關(guān)語義)焚志,阻塞是通過parkAndCheckInterrupt()中的LockSupport.park()實現(xiàn)衣迷。

再看一下釋放鎖:

/**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
        sync.release(1);
    }

release:

/**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
protected final boolean tryRelease(int releases) {
           int c = getState() - releases;
           if (Thread.currentThread() != getExclusiveOwnerThread())
               throw new IllegalMonitorStateException();
           boolean free = false;
           if (c == 0) {
               free = true;
               setExclusiveOwnerThread(null);
           }
           setState(c);
           return free;
       }

可以看出tryRelease和tryAcquire一樣,也是延遲到子類(Sync)實現(xiàn)的酱酬。c==0的時候壶谒,才能成功釋放鎖,所以多次鎖定(看源碼就可以知道lock一次c就+1岳悟,第一張截圖的第二個判斷佃迄,假如是當(dāng)前線程的話就再+一次1)就需要多次釋放才能解鎖。釋放鎖之后贵少,就會喚醒隊列的一個node中的線程

這段代碼目的在于找出第一個可以unpark的線程呵俏,一般說來head.next == head,Head就是第一個線程滔灶,但Head.next可能會被置為null(參考acquireQueued()源碼)普碎,因此比較穩(wěn)妥的辦法是從后往前找第一個可用線程。

/**
    * Wakes up node's successor, if one exists.
    *
    * @param node the node
    */
   private void unparkSuccessor(Node node) {
       /*
        * If status is negative (i.e., possibly needing signal) try
        * to clear in anticipation of signalling.  It is OK if this
        * fails or if status is changed by waiting thread.
        */
       int ws = node.waitStatus;
       if (ws < 0)
           compareAndSetWaitStatus(node, ws, 0);
       /*
        * Thread to unpark is held in successor, which is normally
        * just the next node.  But if cancelled or apparently null,
        * traverse backwards from tail to find the actual
        * non-cancelled successor.
        */
       Node s = node.next;
       if (s == null || s.waitStatus > 0) {
           s = null;
           for (Node t = tail; t != null && t != node; t = t.prev)
               if (t.waitStatus <= 0)
                   s = t;
       }
       if (s != null)
           LockSupport.unpark(s.thread);
   }

其實我們在設(shè)計代碼的時候也是可以通過靜態(tài)內(nèi)部類的方式來實現(xiàn)一些自己想要的功能录平,不過我們經(jīng)常會用Spring框架麻车,其通過動態(tài)代理已經(jīng)實現(xiàn)了這個按需的延遲加載這些特性,也無須去頭疼這些那些的斗这。

其實關(guān)鍵點也就這些动猬,繞來繞去其實就一句話,假如有A和B兩個線程表箭,A符合期望的話赁咙,那么A就可以入主東宮了,B還老老實實的做它的嬪妃就是。

通過以上這些解釋彼水,其實我們發(fā)現(xiàn)崔拥,鎖的底層其實也是在反復(fù)操作一個volatile 變量,而多線程的其他操作也是基于volatile 的特性來實現(xiàn)的凤覆,包括計數(shù)器链瓦,barrier,各種安全工具類盯桦,理解這個其他自然都不是什么問題慈俯,包括很多并發(fā)框架的和事務(wù)等的設(shè)計,先就扯到這里吧俺附。

參考文獻:

Java并發(fā)編程的藝術(shù)


在學(xué)習(xí)過程如果有任何疑問肥卡,請來極樂網(wǎng)
提問溪掀,或者掃描下方二維碼事镣,關(guān)注極樂官方微信,在平臺下方留言揪胃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末璃哟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子喊递,更是在濱河造成了極大的恐慌随闪,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骚勘,死亡現(xiàn)場離奇詭異铐伴,居然都是意外死亡,警方通過查閱死者的電腦和手機俏讹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門当宴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泽疆,你說我怎么就攤上這事户矢。” “怎么了殉疼?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵梯浪,是天一觀的道長。 經(jīng)常有香客問我瓢娜,道長挂洛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任眠砾,我火速辦了婚禮虏劲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己伙单,他們只是感情好获高,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吻育,像睡著了一般念秧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上布疼,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天摊趾,我揣著相機與錄音,去河邊找鬼游两。 笑死砾层,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贱案。 我是一名探鬼主播肛炮,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宝踪!你這毒婦竟也來了侨糟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤瘩燥,失蹤者是張志新(化名)和其女友劉穎秕重,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厉膀,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡溶耘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了服鹅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凳兵。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖菱魔,靈堂內(nèi)的尸體忽然破棺而出留荔,到底是詐尸還是另有隱情,我是刑警寧澤澜倦,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布聚蝶,位于F島的核電站,受9級特大地震影響藻治,放射性物質(zhì)發(fā)生泄漏碘勉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一桩卵、第九天 我趴在偏房一處隱蔽的房頂上張望验靡。 院中可真熱鬧倍宾,春花似錦、人聲如沸胜嗓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辞州。三九已至怔锌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間变过,已是汗流浹背埃元。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留媚狰,地道東北人岛杀。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像崭孤,于是被迫代替她去往敵國和親类嗤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

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

  • Java8張圖 11裳瘪、字符串不變性 12土浸、equals()方法罪针、hashCode()方法的區(qū)別 13彭羹、...
    Miley_MOJIE閱讀 3,707評論 0 11
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司泪酱,掛了不少派殷,但最終還是拿到小米、百度墓阀、阿里毡惜、京東、新浪斯撮、CVTE经伙、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,254評論 11 349
  • 第三章 Java內(nèi)存模型 3.1 Java內(nèi)存模型的基礎(chǔ) 通信在共享內(nèi)存的模型里,通過寫-讀內(nèi)存中的公共狀態(tài)進行隱...
    澤毛閱讀 4,356評論 2 22
  • 第一次月考落幕勿锅,幾家歡喜幾家愁呀帕膜! 一百五十分試卷只得二十四分。令人無語溢十。作文直接抄試卷閱讀段垮刹,一字不拉,但錯別字...
    鑫享人生的窩閱讀 268評論 0 0
  • 一张弛、今日計劃 我今天的任務(wù)根據(jù)優(yōu)先級安排如下: 1.閱讀永澄文章 預(yù)計成果:腦圖1篇 預(yù)計時間:1h 實際時間:1...
    樓上的藝術(shù)家閱讀 167評論 0 0