搞懂Java 的幾把 JVM 級鎖

個人博客請訪問 http://www.x0100.top   

簡介

在計算機行業有一個定律叫"摩爾定律",在此定律下,計算機的性能突飛猛進,而且價格也隨之越來越便宜, CPU 從單核到了多核,緩存性能也得到了很大提升,尤其是多核 CPU 技術的到來,計算機同一時刻可以處理多個任務。在硬件層面的發展帶來的效率極大提升中,軟件層面的多線程編程已經成爲必然趨勢,然而多線程編程就會引入數據安全性問題,有矛必有盾,於是發明了“鎖”來解決線程安全問題。在這篇文章中,總結了 Java 中幾把經典的 JVM 級別的鎖。

synchronized

synchronized 關鍵字是一把經典的鎖,也是我們平時用得最多的。在 JDK1.6 之前, syncronized 是一把重量級的鎖,不過隨着 JDK 的升級,也在對它進行不斷的優化,如今它變得不那麼重了,甚至在某些場景下,它的性能反而優於輕量級鎖。在加了 syncronized 關鍵字的方法、代碼塊中,一次只允許一個線程進入特定代碼段,從而避免多線程同時修改同一數據。

synchronized 鎖有如下幾個特點:

有鎖升級過程

在 JDK1.5 (含)之前, synchronized 的底層實現是重量級的,所以之前一致稱呼它爲"重量級鎖",在 JDK1.5 之後,對 synchronized 進行了各種優化,它變得不那麼重了,實現原理就是鎖升級的過程。我們先聊聊 1.5 之後的 synchronized 實現原理是怎樣的。說到 synchronized 加鎖原理,就不得不先說 Java 對象在內存中的佈局, Java 對象內存佈局如下:

如上圖所示,在創建一個對象後,在 JVM 虛擬機( HotSpot )中,對象在 Java 內存中的存儲佈局 可分爲三塊:

對象頭區域此處存儲的信息包括兩部分:

1、對象自身的運行時數據( MarkWord )

存儲 hashCode、GC 分代年齡、鎖類型標記、偏向鎖線程 ID 、 CAS 鎖指向線程 LockRecord 的指針等, synconized 鎖的機制與這個部分( markwork )密切相關,用 markword 中最低的三位代表鎖的狀態,其中一位是偏向鎖位,另外兩位是普通鎖位。

2、對象類型指針( Class Pointer )

對象指向它的類元數據的指針、 JVM 就是通過它來確定是哪個 Class 的實例。

實例數據區域 

 此處存儲的是對象真正有效的信息,比如對象中所有字段的內容

對齊填充區域

 JVM 的實現 HostSpot 規定對象的起始地址必須是 8 字節的整數倍,換句話來說,現在 64 位的 OS 往外讀取數據的時候一次性讀取 64bit 整數倍的數據,也就是 8 個字節,所以 HotSpot 爲了高效讀取對象,就做了"對齊",如果一個對象實際佔的內存大小不是 8byte 的整數倍時,就"補位"到 8byte 的整數倍。所以對齊填充區域的大小不是固定的。

當線程進入到 synchronized 處嘗試獲取該鎖時, synchronized 鎖升級流程如下:

如上圖所示, synchronized 鎖升級的順序爲:偏向鎖->輕量級鎖->重量級鎖,每一步觸發鎖升級的情況如下:

偏向鎖

在 JDK1.8 中,其實默認是輕量級鎖,但如果設定了 -XX:BiasedLockingStartupDelay = 0 ,那在對一個 Object 做 syncronized 的時候,會立即上一把偏向鎖。當處於偏向鎖狀態時, markwork 會記錄當前線程 ID 。

升級到輕量級鎖

當下一個線程參與到偏向鎖競爭時,會先判斷 markword 中保存的線程 ID 是否與這個線程 ID 相等,如果不相等,會立即撤銷偏向鎖,升級爲輕量級鎖。每個線程在自己的線程棧中生成一個 LockRecord ( LR ),然後每個線程通過 CAS (自旋)的操作將鎖對象頭中的 markwork 設置爲指向自己的 LR 的指針,哪個線程設置成功,就意味着獲得鎖。關於 synchronized 中此時執行的 CAS 操作是通過 native 的調用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代碼實現的,有興趣的可以繼續深挖。

升級到重量級鎖

如果鎖競爭加劇(如線程自旋次數或者自旋的線程數超過某閾值, JDK1.6 之後,由 JVM 自己控制該規則),就會升級爲重量級鎖。此時就會向操作系統申請資源,線程掛起,進入到操作系統內核態的等待隊列中,等待操作系統調度,然後映射回用戶態。在重量級鎖中,由於需要做內核態到用戶態的轉換,而這個過程中需要消耗較多時間,也就是"重"的原因之一。

可重入

synchronized 擁有強制原子性的內部鎖機制,是一把可重入鎖。因此,在一個線程使用 synchronized 方法時調用該對象另一個 synchronized 方法,即一個線程得到一個對象鎖後再次請求該對象鎖(即一個線程多次獲取同一對象的鎖),是永遠可以拿到鎖的。在 Java 中線程獲得對象鎖的操作是以線程爲單位的,而不是以調用爲單位的。synchronized 鎖的對象頭的 markwork 中會記錄該鎖的線程持有者和計數器,當一個線程請求成功後, JVM 會記下持有鎖的線程,並將計數器計爲1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增。當線程退出一個  synchronized 方法/塊時,計數器會遞減,如果計數器爲 0 則釋放該鎖鎖。

悲觀鎖(互斥鎖、排他鎖)

 synchronized 是一把悲觀鎖(獨佔鎖),當前線程如果獲取到鎖,會導致其它所有需要鎖的線程等待,一直等待持有鎖的線程釋放鎖才繼續進行鎖的爭搶。

ReentrantLock

ReentrantLock 從字面可以看出是一把可重入鎖,這點和 synchronized 一樣,但實現原理也與 syncronized 有很大差別,它是基於經典的 AQS(AbstractQueueSyncronized) 實現的, AQS 是基於 volitale 和 CAS 實現的,其中 AQS 中維護一個 valitale 類型的變量 state 來做一個可重入鎖的重入次數,加鎖和釋放鎖也是圍繞這個變量來進行的。ReentrantLock 也提供了一些 synchronized 沒有的特點,因此比 synchronized 好用。

AQS模型如下圖:

ReentrantLock 有如下特點:

1、可重入

 ReentrantLock 和 syncronized 關鍵字一樣,都是可重入鎖,不過兩者實現原理稍有差別, RetrantLock 利用 AQS 的的 state 狀態來判斷資源是否已鎖,同一線程重入加鎖, state 的狀態 +1 ; 同一線程重入解鎖, state 狀態 -1 (解鎖必須爲當前獨佔線程,否則異常); 當 state 爲 0 時解鎖成功。

2、需要手動加鎖、解鎖

synchronized 關鍵字是自動進行加鎖、解鎖的,而 ReentrantLock 需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成,來手動加鎖、解鎖。

3、支持設置鎖的超時時間

 synchronized 關鍵字無法設置鎖的超時時間,如果一個獲得鎖的線程內部發生死鎖,那麼其他線程就會一直進入阻塞狀態,而 ReentrantLock 提供 tryLock 方法,允許設置線程獲取鎖的超時時間,如果超時,則跳過,不進行任何操作,避免死鎖的發生。

4、支持公平/非公平鎖

synchronized 關鍵字是一種非公平鎖,先搶到鎖的線程先執行。而 ReentrantLock 的構造方法中允許設置 true/false 來實現公平、非公平鎖,如果設置爲 true ,則線程獲取鎖要遵循"先來後到"的規則,每次都會構造一個線程 Node ,然後到雙向鏈表的"尾巴"後面排隊,等待前面的 Node 釋放鎖資源。

5、可中斷鎖

 ReentrantLock 中的 lockInterruptibly() 方法使得線程可以在被阻塞時響應中斷,比如一個線程 t1 通過 lockInterruptibly() 方法獲取到一個可重入鎖,並執行一個長時間的任務,另一個線程通過 interrupt() 方法就可以立刻打斷 t1 線程的執行,來獲取t1持有的那個可重入鎖。而通過 ReentrantLock 的 lock() 方法或者 Synchronized 持有鎖的線程是不會響應其他線程的 interrupt() 方法的,直到該方法主動釋放鎖之後纔會響應 interrupt() 方法。

ReentrantReadWriteLock

ReentrantReadWriteLock (讀寫鎖)其實是兩把鎖,一把是 WriteLock (寫鎖),一把是讀鎖, ReadLock 。讀寫鎖的規則是:讀讀不互斥、讀寫互斥、寫寫互斥。在一些實際的場景中,讀操作的頻率遠遠高於寫操作,如果直接用一般的鎖進行併發控制的話,就會讀讀互斥、讀寫互斥、寫寫互斥,效率低下,讀寫鎖的產生就是爲了優化這種場景的操作效率。一般情況下獨佔鎖的效率低來源於高併發下對臨界區的激烈競爭導致線程上下文切換。因此當併發不是很高的情況下,讀寫鎖由於需要額外維護讀鎖的狀態,可能還不如獨佔鎖的效率高,因此需要根據實際情況選擇使用。 

ReentrantReadWriteLock 的原理也是基於 AQS 進行實現的,與 ReentrantLock 的差別在於 ReentrantReadWriteLock 鎖擁有共享鎖、排他鎖屬性。讀寫鎖中的加鎖、釋放鎖也是基於 Sync (繼承於 AQS ),並且主要使用 AQS 中的 state 和 node 中的 waitState 變量進行實現的。實現讀寫鎖與實現普通互斥鎖的主要區別在於需要分別記錄讀鎖狀態及寫鎖狀態,並且等待隊列中需要區別處理兩種加鎖操作。ReentrantReadWriteLock 中將 AQS 中的 int 類型的 state 分爲高 16 位與第 16 位分別記錄讀鎖和寫鎖的狀態,如下圖所示:

WriteLock(寫鎖)是悲觀鎖(排他鎖、互斥鎖)

通過計算 state&((1<<16)-1) ,將 state 的高 16 位全部抹去,因此 state 的低位記錄着寫鎖的重入計數。

獲取寫鎖源碼:
 

/**         * 獲取寫鎖           Acquires the write lock.         *  如果此時沒有任何線程持有寫鎖或者讀鎖,那麼當前線程執行CAS操作更新status,         *  若更新成功,則設置讀鎖重入次數爲1,並立即返回         *
Acquires the write lock if neither the read nor write lock         * are held by another thread         * and returns immediately, setting the write lock hold count to         * one.         *  如果當前線程已經持有該寫鎖,那麼將寫鎖持有次數設置爲1,並立即返回         *
If the current thread already holds the write lock then the         * hold count is incremented by one and the method returns         * immediately.         *  如果該鎖已經被另外一個線程持有,那麼停止該線程的CPU調度並進入休眠狀態,         *  直到該寫鎖被釋放,且成功將寫鎖持有次數設置爲1才表示獲取寫鎖成功         *
If the lock is held by another thread then the current         * thread becomes disabled for thread scheduling purposes and         * lies dormant until the write lock has been acquired, at which         * time the write lock hold count is set to one.         */        public void lock() {            sync.acquire(1);        }/**     * 該方法爲以獨佔模式獲取鎖,忽略中斷     * 如果調用一次該“tryAcquire”方法更新status成功,則直接返回,代表搶鎖成功     * 否則,將會進入同步隊列等待,不斷執行“tryAcquire”方法嘗試CAS更新status狀態,直到成功搶到鎖     


其中“tryAcquire”方法在NonfairSync(公平鎖)中和FairSync(非公平鎖)中都有各自的實現

*     * 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();    }    protected final boolean tryAcquire(int acquires) {            /*             * Walkthrough:             *

1、如果讀寫鎖的計數不爲0,且持有鎖的線程不是當前線程,則返回false     

  * 1. If read count nonzero or write count nonzero             *    and owner is a different thread, fail.             *

2、如果持有鎖的計數不爲0且計數總數超過限定的最大值,也返回false            

*2. If count would saturate, fail. (This can only             *    happen if count is already nonzero.)             *

3、如果該鎖是可重入或該線程在隊列中的策略是允許它嘗試搶鎖,那麼該線程就能獲取鎖             *

Otherwise, this thread is eligible for lock if             *    it is either a reentrant acquire or             *    queue policy allows it. If so, update state             *    and set owner.             */            Thread current = Thread.currentThread();            //獲取讀寫鎖的狀態            int c = getState();            //獲取該寫鎖重入的次數            int w = exclusiveCount(c);            //如果讀寫鎖狀態不爲0,說明已經有其他線程獲取了讀鎖或寫鎖            if (c != 0) {                //如果寫鎖重入次數爲0,說明有線程獲取到讀鎖,根據“讀寫鎖互斥”原則,返回false                //或者如果寫鎖重入次數不爲0,且獲取寫鎖的線程不是當前線程,根據"寫鎖獨佔"原則,返回false                // (Note: if c != 0 and w == 0 then shared count != 0)                if (w == 0 || current != getExclusiveOwnerThread())                    return false;               //如果寫鎖可重入次數超過最大次數(65535),則拋異常                if (w + exclusiveCount(acquires) > MAX_COUNT)                    throw new Error("Maximum lock count exceeded");                //到這裏說明該線程是重入寫鎖,更新重入寫鎖的計數(+1),返回true                // Reentrant acquire                setState(c + acquires);                return true;            }            //如果讀寫鎖狀態爲0,說明讀鎖和寫鎖都沒有被獲取,會走下面兩個分支:           //如果要阻塞或者執行CAS操作更新讀寫鎖的狀態失敗,則返回false            //如果不需要阻塞且CAS操作成功,則當前線程成功拿到鎖,設置鎖的owner爲當前線程,返回true            if (writerShouldBlock() ||                !compareAndSetState(c, c + acquires))                return false;            setExclusiveOwnerThread(current);            return true;        }

釋放寫鎖源碼:

/*  * Note that tryRelease and tryAcquire can be called by  * Conditions. So it is possible that their arguments contain  * both read and write holds that are all released during a  * condition wait and re-established in tryAcquire.  */ protected final boolean tryRelease(int releases) {     //若鎖的持有者不是當前線程,拋出異常     if (!isHeldExclusively())         throw new IllegalMonitorStateException();     //寫鎖的可重入計數減掉releases個     int nextc = getState() - releases;     //如果寫鎖重入計數爲0了,則說明寫鎖被釋放了     boolean free = exclusiveCount(nextc) == 0;     if (free)        //若寫鎖被釋放,則將鎖的持有者設置爲null,進行GC        setExclusiveOwnerThread(null);     //更新寫鎖的重入計數     setState(nextc);     return free; }

ReadLock(讀鎖)是共享鎖(樂觀鎖)

通過計算 state>>>16 進行無符號補 0 ,右移 16 位,因此 state 的高位記錄着寫鎖的重入計數.

讀鎖獲取鎖的過程比寫鎖稍微複雜些,首先判斷寫鎖是否爲 0 並且當前線程不佔有獨佔鎖,直接返回;否則,判斷讀線程是否需要被阻塞並且讀鎖數量是否小於最大值並且比較設置狀態成功,若當前沒有讀鎖,則設置第一個讀線程 firstReader 和 firstReaderHoldCount ;若當前線程線程爲第一個讀線程,則增加 firstReaderHoldCount ;否則,將設置當前線程對應的 HoldCounter 對象的值,更新成功後會在 firstReaderHoldCount 中 readHolds ( ThreadLocal 類型的)的本線程副本中記錄當前線程重入數,這是爲了實現 JDK1.6 中加入的 getReadHoldCount ()方法的,這個方法能獲取當前線程重入共享鎖的次數( state 中記錄的是多個線程的總重入次數),加入了這個方法讓代碼複雜了不少,但是其原理還是很簡單的:如果當前只有一個線程的話,還不需要動用 ThreadLocal ,直接往 firstReaderHoldCount 這個成員變量裏存重入數,當有第二個線程來的時候,就要動用 ThreadLocal 變量 readHolds 了,每個線程擁有自己的副本,用來保存自己的重入數。

獲取讀鎖源碼: 

/**         * 獲取讀鎖         * Acquires the read lock.         * 如果寫鎖未被其他線程持有,執行CAS操作更新status值,獲取讀鎖後立即返回         *
Acquires the read lock if the write lock is not held by         * another thread and returns immediately.         *         * 如果寫鎖被其他線程持有,那麼停止該線程的CPU調度並進入休眠狀態,直到該讀鎖被釋放         *
If the write lock is held by another thread then         * the current thread becomes disabled for thread scheduling         * purposes and lies dormant until the read lock has been acquired.         */        public void lock() {            sync.acquireShared(1);        }   /**     * 該方法爲以共享模式獲取讀鎖,忽略中斷     * 如果調用一次該“tryAcquireShared”方法更新status成功,則直接返回,代表搶鎖成功     * 否則,將會進入同步隊列等待,不斷執行“tryAcquireShared”方法嘗試CAS更新status狀態,直到成功搶到鎖     * 其中“tryAcquireShared”方法在NonfairSync(公平鎖)中和FairSync(非公平鎖)中都有各自的實現     * (看這注釋是不是和寫鎖很對稱)     * Acquires in shared mode, ignoring interrupts.  Implemented by     * first invoking at least once {@link #tryAcquireShared},     * returning on success.  Otherwise the thread is queued, possibly     * repeatedly blocking and unblocking, invoking {@link     * #tryAcquireShared} until success.     *     * @param arg the acquire argument.  This value is conveyed to     *        {@link #tryAcquireShared} but is otherwise uninterpreted     *        and can represent anything you like.     */    public final void acquireShared(int arg) {        if (tryAcquireShared(arg) < 0)            doAcquireShared(arg);    }    protected final int tryAcquireShared(int unused) {            /*             * Walkthrough:             * 1、如果已經有其他線程獲取到了寫鎖,根據“讀寫互斥”原則,搶鎖失敗,返回-1             * 1.If write lock held by another thread, fail.             * 2、如果該線程本身持有寫鎖,那麼看一下是否要readerShouldBlock,如果不需要阻塞,             *    則執行CAS操作更新state和重入計數。*    這裏要注意的是,上面的步驟不檢查是否可重入(因爲讀鎖屬於共享鎖,天生支持可重入)             * 2. Otherwise, this thread is eligible for             *    lock wrt state, so ask if it should block             *    because of queue policy. If not, try             *    to grant by CASing state and updating count.             *    Note that step does not check for reentrant             *    acquires, which is postponed to full version             *    to avoid having to check hold count in             *    the more typical non-reentrant case.             * 3、如果因爲CAS更新status失敗或者重入計數超過最大值導致步驟2執行失敗             *    那就進入到fullTryAcquireShared方法進行死循環,直到搶鎖成功             * 3. If step 2 fails either because thread             *    apparently not eligible or CAS fails or count             *    saturated, chain to version with full retry loop.             */            //當前嘗試獲取讀鎖的線程            Thread current = Thread.currentThread();            //獲取該讀寫鎖狀態            int c = getState();            //如果有線程獲取到了寫鎖 ,且獲取寫鎖的不是當前線程則返回失敗            if (exclusiveCount(c) != 0 &&                getExclusiveOwnerThread() != current)                return -1;            //獲取讀鎖的重入計數            int r = sharedCount(c);            //如果讀線程不應該被阻塞,且重入計數小於最大值,且CAS執行讀鎖重入計數+1成功,則執行線程重入的計數加1操作,返回成功            if (!readerShouldBlock() &&                r < MAX_COUNT &&                compareAndSetState(c, c + SHARED_UNIT)) {                //如果還未有線程獲取到讀鎖,則將firstReader設置爲當前線程,firstReaderHoldCount設置爲1                if (r == 0) {                    firstReader = current;                    firstReaderHoldCount = 1;                } else if (firstReader == current) {                    //如果firstReader是當前線程,則將firstReader的重入計數變量firstReaderHoldCount加1                    firstReaderHoldCount++;                } else {                    //否則說明有至少兩個線程共享讀鎖,獲取共享鎖重入計數器HoldCounter                    //從HoldCounter中拿到當前線程的線程變量cachedHoldCounter,將此線程的重入計數count加1                    HoldCounter rh = cachedHoldCounter;                    if (rh == null || rh.tid != getThreadId(current))                        cachedHoldCounter = rh = readHolds.get();                    else if (rh.count == 0)                        readHolds.set(rh);                    rh.count++;                }                return 1;            }            //如果上面的if條件有一個都不滿足,則進入到這個方法裏進行死循環重新獲取            return fullTryAcquireShared(current);        }        /**         * 用於處理CAS操作state失敗和tryAcquireShared中未執行獲取可重入鎖動作的full方法(補償方法?)         * Full version of acquire for reads, that handles CAS misses         * and reentrant reads not dealt with in tryAcquireShared.         */        final int fullTryAcquireShared(Thread current) {            /*             * 此代碼與tryAcquireShared中的代碼有部分相似的地方,             * 但總體上更簡單,因爲不會使tryAcquireShared與重試和延遲讀取保持計數之間的複雜判斷             * This code is in part redundant with that in             * tryAcquireShared but is simpler overall by not             * complicating tryAcquireShared with interactions between             * retries and lazily reading hold counts.             */            HoldCounter rh = null;            //死循環            for (;;) {                //獲取讀寫鎖狀態                int c = getState();                //如果有線程獲取到了寫鎖                if (exclusiveCount(c) != 0) {                    //如果獲取寫鎖的線程不是當前線程,返回失敗                    if (getExclusiveOwnerThread() != current)                        return -1;                    // else we hold the exclusive lock; blocking here                    // would cause deadlock.                } else if (readerShouldBlock()) {//如果沒有線程獲取到寫鎖,且讀線程要阻塞                    // Make sure we're not acquiring read lock reentrantly                    //如果當前線程爲第一個獲取到讀鎖的線程                    if (firstReader == current) {                        // assert firstReaderHoldCount > 0;                    } else { //如果當前線程不是第一個獲取到讀鎖的線程(也就是說至少有有一個線程獲取到了讀鎖)                        //                        if (rh == null) {                            rh = cachedHoldCounter;                            if (rh == null || rh.tid != getThreadId(current)) {                                rh = readHolds.get();                                if (rh.count == 0)                                    readHolds.remove();                            }                        }                        if (rh.count == 0)                            return -1;                    }                }                /**                 *下面是既沒有線程獲取寫鎖,當前線程又不需要阻塞的情況                 */                //重入次數等於最大重入次數,拋異常                if (sharedCount(c) == MAX_COUNT)                    throw new Error("Maximum lock count exceeded");                //如果執行CAS操作成功將讀寫鎖的重入計數加1,則對當前持有這個共享讀鎖的線程的重入計數加1,然後返回成功                if (compareAndSetState(c, c + SHARED_UNIT)) {                    if (sharedCount(c) == 0) {                        firstReader = current;                        firstReaderHoldCount = 1;                    } else if (firstReader == current) {                        firstReaderHoldCount++;                    } else {                        if (rh == null)                            rh = cachedHoldCounter;                        if (rh == null || rh.tid != getThreadId(current))                            rh = readHolds.get();                        else if (rh.count == 0)                            readHolds.set(rh);                        rh.count++;                        cachedHoldCounter = rh; // cache for release                    }                    return 1;                }            }        }

釋放讀鎖源碼:

/**  * Releases in shared mode.  Implemented by unblocking one or more  * threads if {@link #tryReleaseShared} returns true.  *  * @param arg the release argument.  This value is conveyed to  *        {@link #tryReleaseShared} but is otherwise uninterpreted  *        and can represent anything you like.  * @return the value returned from {@link #tryReleaseShared}  */public final boolean releaseShared(int arg) {    if (tryReleaseShared(arg)) {//嘗試釋放一次共享鎖計數        doReleaseShared();//真正釋放鎖        return true;    }        return false;}/** *此方法表示讀鎖線程釋放鎖。*首先判斷當前線程是否爲第一個讀線程firstReader, *若是,則判斷第一個讀線程佔有的資源數firstReaderHoldCount是否爲1,  若是,則設置第一個讀線程firstReader爲空,否則,將第一個讀線程佔有的資源數firstReaderHoldCount減1;若當前線程不是第一個讀線程,  那麼首先會獲取緩存計數器(上一個讀鎖線程對應的計數器 ),  若計數器爲空或者tid不等於當前線程的tid值,則獲取當前線程的計數器,  如果計數器的計數count小於等於1,則移除當前線程對應的計數器,  如果計數器的計數count小於等於0,則拋出異常,之後再減少計數即可。無論何種情況,都會進入死循環,該循環可以確保成功設置狀態state */protected final boolean tryReleaseShared(int unused) {      // 獲取當前線程      Thread current = Thread.currentThread();      if (firstReader == current) { // 當前線程爲第一個讀線程          // assert firstReaderHoldCount > 0;         if (firstReaderHoldCount == 1) // 讀線程佔用的資源數爲1              firstReader = null;          else // 減少佔用的資源              firstReaderHoldCount--;     } else { // 當前線程不爲第一個讀線程         // 獲取緩存的計數器         HoldCounter rh = cachedHoldCounter;         if (rh == null || rh.tid != getThreadId(current)) // 計數器爲空或者計數器的tid不爲當前正在運行的線程的tid             // 獲取當前線程對應的計數器             rh = readHolds.get();         // 獲取計數         int count = rh.count;         if (count <= 1) { // 計數小於等於1             // 移除             readHolds.remove();             if (count <= 0) // 計數小於等於0,拋出異常                 throw unmatchedUnlockException();         }         // 減少計數         --rh.count;     }     for (;;) { // 死循環         // 獲取狀態         int c = getState();         // 獲取狀態         int nextc = c - SHARED_UNIT;         if (compareAndSetState(c, nextc)) // 比較並進行設置             // Releasing the read lock has no effect on readers,             // but it may allow waiting writers to proceed if             // both read and write locks are now free.             return nextc == 0;     } } /**真正釋放鎖  * Release action for shared mode -- signals successor and ensures  * propagation. (Note: For exclusive mode, release just amounts  * to calling unparkSuccessor of head if it needs signal.)  */private void doReleaseShared() {        /*         * Ensure that a release propagates, even if there are other         * in-progress acquires/releases.  This proceeds in the usual         * way of trying to unparkSuccessor of head if it needs         * signal. But if it does not, status is set to PROPAGATE to         * ensure that upon release, propagation continues.         * Additionally, we must loop in case a new node is added         * while we are doing this. Also, unlike other uses of         * unparkSuccessor, we need to know if CAS to reset status         * fails, if so rechecking.         */        for (;;) {            Node h = head;            if (h != null && h != tail) {                int ws = h.waitStatus;                if (ws == Node.SIGNAL) {                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))                        continue;            // loop to recheck cases                    unparkSuccessor(h);                }                else if (ws == 0 &&                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))                    continue;                // loop on failed CAS            }            if (h == head)                   // loop if head changed                break;        }    }

通過分析可以看出:

在線程持有讀鎖的情況下,該線程不能取得寫鎖(因爲獲取寫鎖的時候,如果發現當前的讀鎖被佔用,就馬上獲取失敗,不管讀鎖是不是被當前線程持有)。

 

在線程持有寫鎖的情況下,該線程可以繼續獲取讀鎖(獲取讀鎖時如果發現寫鎖被佔用,只有寫鎖沒有被當前線程佔用的情況纔會獲取失敗)。

 

LongAdder

 

在高併發的情況下,我們對一個 Integer 類型的整數直接進行 i++ 的時候,無法保證操作的原子性,會出現線程安全的問題。爲此我們會用 juc 下的 AtomicInteger ,它是一個提供原子操作的 Interger 類,內部也是通過 CAS 實現線程安全的。但當大量線程同時去訪問時,就會因爲大量線程執行 CAS 操作失敗而進行空旋轉,導致 CPU 資源消耗過多,而且執行效率也不高。Doug Lea 大神應該也不滿意,於是在 JDK1.8 中對 CAS 進行了優化,提供了 LongAdder ,它是基於了 CAS 分段鎖的思想實現的。

 

線程去讀寫一個 LongAdder 類型的變量時,流程如下:

 

 

 LongAdder 也是基於 Unsafe 提供的 CAS 操作 +valitale 去實現的。在 LongAdder 的父類 Striped64 中維護着一個 base 變量和一個 cell 數組,當多個線程操作一個變量的時候,先會在這個 base 變量上進行 cas 操作,當它發現線程增多的時候,就會使用 cell 數組。比如當 base 將要更新的時候發現線程增多(也就是調用 casBase 方法更新 base 值失敗),那麼它會自動使用 cell 數組,每一個線程對應於一個 cell ,在每一個線程中對該 cell 進行 cas 操作,這樣就可以將單一 value 的更新壓力分擔到多個 value 中去,降低單個 value 的 “熱度”,同時也減少了大量線程的空轉,提高併發效率,分散併發壓力。這種分段鎖需要額外維護一個內存空間 cells ,不過在高併發場景下,這點成本幾乎可以忽略。分段鎖是一種優秀的優化思想, juc 中提供的的 ConcurrentHashMap 也是基於分段鎖保證讀寫操作的線程安全。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章