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

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


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

底層實現(xiàn)原理

有volatile變量修飾的共享變量進(jìn)行寫操作的時候會多出第二行匯編代碼,通過查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)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完不知道何時會寫到內(nèi)存。如果對聲明了volatile的變量進(jìn)行寫操作,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ù)進(jìn)行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。

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

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

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

對象頭

對象頭:包括兩部分信息。第一部分用于存儲對象自身的運(yùn)行時數(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,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。

進(jìn)入正題

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

程序計數(shù)器PC

程序計數(shù)器即指令地址寄存器。在某些計算機(jī)中用來存放當(dāng)前正在執(zhí)行的指令地址;而在另一些計算機(jī)中則用來存放即將要執(zhí)行的下一條指令地址;而在有指令預(yù)取功能的計算機(jī)中,一般還要增加一個程序計數(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源碼來具體對其進(jìn)行分析:


對于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è)計,先就扯到這里吧。

參考文獻(xiàn):

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


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容