淺談一下Java中的幾種JVM級別的鎖
前言
根據摩爾定律,計算機的性能將繼續(xù)飆升,因為計算基礎設施的相關成本將隨著時間的推移繼續(xù)下降。
具體到CPU,已經從簡單的單核系統(tǒng)發(fā)展到多核系統(tǒng),緩存性能也有了飛躍性的提升。隨著多核 CPU 的出現(xiàn),計算機現(xiàn)在可以同時運行多個任務。
并且,隨著硬件開發(fā)的多項提升帶來的顯著效率提升,軟件層面的多線程編程已經成為必然趨勢。
然而,多線程編程也帶來了一些數(shù)據安全問題。
隨著所有這些趨勢的發(fā)展,業(yè)界已經認識到,當存在安全漏洞時,也必須有相應的防護措施。
順應這種趨勢,虛擬“鎖”被發(fā)明出來,以解決線程的安全問題。
在這篇文章中,我們將研究多年來出現(xiàn)的 Java 中幾種典型的 JVM 級鎖。
1、synchronized
synchronized 關鍵字是 Java 中經典且非常典型的鎖。
事實上,它也是最常用的一種。在 JDK 1.6 之前,“synchronized”是一個相當“重量級”的鎖。
不過隨著Java的更新升級,這個鎖也在不斷的優(yōu)化。
如今,這把鎖變得不那么“沉重”了。而且,事實上,在某些場景下,它的性能甚至優(yōu)于典型的輕量級鎖。
而且在帶有synchronized關鍵字的方法和代碼塊中,同一時刻只允許一個線程訪問特定的代碼段,防止多個線程并發(fā)修改同一塊數(shù)據。
1.1)鎖升級
在JDK 1.5(含)之前,synchronized關鍵字的底層實現(xiàn)比較重,因此被稱為“重量級鎖”。
不過在JDK 1.5之后,對“synchronized”鎖進行了各種改進,變得不那么重量級了。
因此,實現(xiàn)方法就是鎖升級過程。我們先看看JDK 1.5之后“同步”鎖是怎么實現(xiàn)的。
說到同步鎖的原理,首先要了解Java對象在內存中的布局。
如上圖所示,創(chuàng)建一個對象后,該對象在JVM HotSpot虛擬機的Java內存中的存儲布局可以分為三種。
1)對象頭
該區(qū)域存儲的信息分為兩部分。
對象本身的運行時數(shù)據(MarkWord):該數(shù)據存儲hashCode、垃圾收集(GC)世代年齡、鎖類型標志、偏向鎖線程的ID、指向LockRecord的Compare and Swap(CAS)鎖指針的線程,除了其他信息。 synchronized鎖的機制與這部分(MarkWord)高度相關。 MarkWord 的最低三位表示鎖定狀態(tài)。對于這三個位,其中一個是偏向鎖定位,另外兩個是普通鎖定位。對象的類指針(Class Pointer):對象的指針是指向其類元數(shù)據的指針。 JVM 使用它來確定實例的類。
2)實例數(shù)據區(qū)
該區(qū)域存放對象的有效信息,如對象中所有字段的內容。
3)對齊填充
JVM HotSpot的實現(xiàn)規(guī)定對象的起始地址必須是8字節(jié)的整數(shù)倍。也就是說,64位操作系統(tǒng)一次讀取的數(shù)據是64位的整數(shù)倍,即8個字節(jié)。因此,HotSpot 進行“對齊”以高效地讀取對象。如果對象的實際內存大小不是 8 字節(jié)的整數(shù)倍,HotSpot 會將對象“填充”為 8 字節(jié)的整數(shù)倍。因此,對齊和填充區(qū)域的大小是動態(tài)的。
1.2)synchronized鎖升級
當線程進入synchronized狀態(tài)并嘗試獲取鎖時,synchronized鎖的升級過程如下。
總結一下,同步鎖的升級順序是:偏向鎖>輕量級鎖>重量級鎖。
The detailed triggering of lock upgrade in each step is as follows.
1)Biased Lock:(偏向鎖)
在 JDK 1.8 中,默認是輕量級鎖。但是,通過設置 -XX:BiasedLockingStartupDelay = 0,在同步對象后立即附加偏向鎖。當線程處于偏向鎖狀態(tài)時,MarkWord記錄當前線程的ID。
2)Upgrading to the Lightweight Lock:(升級到輕量級鎖) 當下一個線程競爭偏向鎖時,系統(tǒng)首先檢查MarkWord中存儲的線程ID是否與本線程的ID一致。如果不是,系統(tǒng)立即撤銷偏向鎖,升級為輕量級鎖。每個線程在自己的線程棧中生成一個LockRecord(LR)。然后,每個線程通過CAS操作(自旋)將鎖對象頭中的MarkWord設置為指向自己LR的指針。如果一個線程成功設置了MarkWord,那么該線程就獲得了鎖。因此,為“同步”執(zhí)行的 CAS 操作是通過 HotSpot 的 bytecodeInterpreter.cpp 文件中的 C++ 代碼完成的。
3)Upgrading to the Heavyweight Lock:(升級到重量級鎖)
如果鎖競爭加劇,比如線程自旋數(shù)或者自旋線程數(shù)超過了一個閾值,這個閾值是JVM自己控制的,對于1.6以后的JDK版本,鎖升級為重量級鎖。然后,重量級鎖向操作系統(tǒng)申請資源。
同時,該線程也被掛起,進入操作系統(tǒng)內核態(tài)的等待隊列,等待操作系統(tǒng)對其進行調度,映射回用戶態(tài)。在重量級鎖中,需要從內核態(tài)轉換到用戶態(tài),這個過程需要比較長的時間,這也是它被定性為“重量級”的原因之一。
1.3)其他:
1)可重入性:
synchronized鎖內部有一個強制原子性的鎖機制,就是可重入鎖。當一個線程使用synchronized方法時,會調用該對象的另一個synchronized方法。即線程獲得對象鎖后,一旦再次請求該對象鎖,該線程總能獲得該鎖。在Java中,線程獲取對象鎖的操作是基于線程的,而不是基于調用的。
synchrnoized塊、語句中所對應的對象頭中的MarkWord記錄了鎖的線程持有者和計數(shù)器。當一個線程請求成功后,JVM記錄持有鎖的線程,并將計數(shù)置為1。此時,如果有另一個線程請求鎖,則該線程必須等待。
當持有鎖的線程再次請求鎖時,可以再次獲得鎖,計數(shù)隨之遞增。當線程退出同步方法或塊時,計數(shù)會遞減。最后,如果計數(shù)為 0,則釋放鎖。
2)Pessimistic Lock (Mutex and Exclusive Lock):(悲觀鎖(互斥和排他鎖))
synchronized鎖是悲觀鎖,更確切地說是排他鎖。換句話說,如果當前線程獲得了鎖,任何其他需要鎖的線程都必須等待。鎖競爭繼續(xù),直到持有鎖的線程釋放鎖。
2、ReentrantLock
ReentrantLock 和synchronized類似,但其實現(xiàn)方式與同步鎖有較大區(qū)別。具體來說,它是基于經典的AbstractQueueSyncronized(AQS)實現(xiàn)的。 AQS是基于volatile和CAS實現(xiàn)的。 AQS維護volatile類型的state變量來統(tǒng)計重入鎖的重入嘗試次數(shù)。同樣,加鎖和釋放鎖也是基于這個變量。 ReentrantLock提供了一些synchronized鎖所沒有的額外特性,所以比synchronized鎖要好。
2.1)ReentrantLock特性:
1)可重入:
ReentrantLock 和 synchronized 關鍵字一樣,都是支持重入的鎖。但是,它們的實現(xiàn)方法略有不同。 ReentrantLock通過AQS的狀態(tài)來判斷資源是否已經被鎖定。對于同一個線程,如果被鎖定,狀態(tài)值加1,如果被解鎖,狀態(tài)值減1。注意解鎖只對當前獨占線程有效,否則會出現(xiàn)異常。如果狀態(tài)值為0,則解鎖成功。
2)手動加鎖、解鎖:
synchronized 關鍵字自動鎖定和解鎖。相比之下,ReentrantLock 需要 lock() 和 unlock() 方法以及 try/finally 語句塊來手動鎖定和解鎖。
3)lock timeout:
synchronized 關鍵字不能設置鎖定超時時間。如果在獲取鎖的線程中發(fā)生死鎖,其他線程將保持阻塞狀態(tài)。 ReentrantLock 提供了 tryLock 方法,可以為獲得鎖的線程設置超時時間。如果超過超時時間,則跳過該線程,不執(zhí)行任何操作,從而防止死鎖。
4)Fair and Unfair Locks
synchronized 關鍵字是一種非公平鎖,第一個搶到鎖的線程先運行。通過在ReentrantLock的構造方法中設置true或false,可以實現(xiàn)公平鎖和非公平鎖。如果設置為true,線程需要遵循“先到先得”的規(guī)則。每次線程要獲取鎖時,都會構造一個線程節(jié)點,然后追加到雙向鏈表的“尾部”進行排隊,等待隊列中的前一個節(jié)點釋放鎖資源。
5)可中斷:
ReentrantLock 中的 lockInterruptibly() 方法允許線程在被阻塞時響應中斷。比如線程t1通過lockInterruptibly()方法獲得了一個ReentrantLock,運行了一個long task。其他線程可以使用interrupt()方法立即中斷線程t1的運行,獲得線程t1的ReentrantLock。但是,ReentrantLock的lock()方法或者有synchronized鎖的線程是不會響應其他線程的interrupt()方法的,直到這個方法主動釋放鎖。
2.2)ReentrantReadWriteLock
ReentrantReadWriteLock(讀寫鎖)其實就是兩把鎖,一把是WriteLock(寫鎖),一把是ReadLock(讀鎖)。讀寫鎖的規(guī)則是:read-read non-exclusive, read-write exclusive, and write-write exclusive。在一些實際場景中,讀的頻率遠高于寫的頻率。如果使用普通鎖進行并發(fā)控制,讀寫互斥,效率低下。
為了優(yōu)化這種場景下的運行效率,讀寫鎖應運而生??偟膩碚f,獨占鎖的低效率來源于高并發(fā)下臨界區(qū)的激烈競爭,導致線程上下文切換。當并發(fā)不是很高的時候,讀寫鎖的效率可能會低于排他鎖,因為需要額外維護。因此,您需要根據實際情況選擇合適的鎖具。
ReentrantReadWriteLock也是基于AQS實現(xiàn)的。 ReentrantLock 和 ReentrantReadWriteLock 的區(qū)別在于后者具有共享鎖和排它鎖的屬性。讀寫鎖中的加鎖和解鎖是基于Sync的,繼承自AQS。主要由AQS中的state和node中的waitState變量實現(xiàn)。
讀寫鎖的實現(xiàn)與普通互斥鎖的主要區(qū)別在于需要分別記錄讀鎖狀態(tài)和寫鎖狀態(tài),等待隊列需要區(qū)別對待這兩種鎖操作。在ReentrantReadWriteLock中,AQS中的int型狀態(tài)分為高16位和低16位分別記錄讀鎖和寫鎖狀態(tài),如下圖。
1)The WriteLock (Write Lock) Is a Pessimistic Lock (Exclusive Lock or Mutex)
通過計算state&((1<<16)-1),將state的高16位全部擦除。因此,狀態(tài)的低位記錄了寫鎖的重入次數(shù)。
下面是獲取寫鎖的源碼。
/** * 獲取寫鎖 Acquires the write lock. * 如果此時沒有任何線程持有寫鎖或者讀鎖,那么當前線程執(zhí)行CAS操作更新status, * 若更新成功,則設置讀鎖重入次數(shù)為1,并立即返回 * <p>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. * 如果當前線程已經持有該寫鎖,那么將寫鎖持有次數(shù)設置為1,并立即返回 * <p>If the current thread already holds the write lock then the * hold count is incremented by one and the method returns * immediately. * 如果該鎖已經被另外一個線程持有,那么停止該線程的CPU調度并進入休眠狀態(tài), * 直到該寫鎖被釋放,且成功將寫鎖持有次數(shù)設置為1才表示獲取寫鎖成功 * <p>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成功,則直接返回,代表搶鎖成功 * 否則,將會進入同步隊列等待,不斷執(zhí)行"tryAcquire"方法嘗試CAS更新status狀態(tài),直到成功搶到鎖 * 其中"tryAcquire"方法在NonfairSync(公平鎖)中和FairSync(非公平鎖)中都有各自的實現(xiàn) * * 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、如果讀寫鎖的計數(shù)不為0,且持有鎖的線程不是當前線程,則返回false * 1. If read count nonzero or write count nonzero * and owner is a different thread, fail. * 2、如果持有鎖的計數(shù)不為0且計數(shù)總數(shù)超過限定的最大值,也返回false * 2. If count would saturate, fail. (This can only * happen if count is already nonzero.) * 3、如果該鎖是可重入或該線程在隊列中的策略是允許它嘗試搶鎖,那么該線程就能獲取鎖 * 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(); //獲取讀寫鎖的狀態(tài) int c = getState(); //獲取該寫鎖重入的次數(shù) int w = exclusiveCount(c); //如果讀寫鎖狀態(tài)不為0,說明已經有其他線程獲取了讀鎖或寫鎖 if (c != 0) { //如果寫鎖重入次數(shù)為0,說明有線程獲取到讀鎖,根據"讀寫鎖互斥"原則,返回false //或者如果寫鎖重入次數(shù)不為0,且獲取寫鎖的線程不是當前線程,根據"寫鎖獨占"原則,返回false // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; //如果寫鎖可重入次數(shù)超過最大次數(shù)(65535),則拋異常 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); //到這里說明該線程是重入寫鎖,更新重入寫鎖的計數(shù)(+1),返回true // Reentrant acquire setState(c + acquires); return true; } //如果讀寫鎖狀態(tài)為0,說明讀鎖和寫鎖都沒有被獲取,會走下面兩個分支: //如果要阻塞或者執(zhí)行CAS操作更新讀寫鎖的狀態(tài)失敗,則返回false //如果不需要阻塞且CAS操作成功,則當前線程成功拿到鎖,設置鎖的owner為當前線程,返回true if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
Source code for releasing the write lock:
/* * 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(); //寫鎖的可重入計數(shù)減掉releases個 int nextc = getState() - releases; //如果寫鎖重入計數(shù)為0了,則說明寫鎖被釋放了 boolean free = exclusiveCount(nextc) == 0; if (free) //若寫鎖被釋放,則將鎖的持有者設置為null,進行GC setExclusiveOwnerThread(null); //更新寫鎖的重入計數(shù) setState(nextc); return free; }
2)The ReadLock (Read Lock) Is a Shared Lock (Optimistic Lock)
通過計算無符號補零的狀態(tài)>>>16,引入了 16 個附加位。所以state的高位記錄了寫鎖的重入次數(shù)。
獲取讀鎖的過程比獲取寫鎖稍微復雜一些。首先系統(tǒng)判斷寫鎖的計數(shù)是否為0,當前線程沒有持有排他鎖。如果是,系統(tǒng)直接返回結果。如果不是,系統(tǒng)檢查讀線程是否需要阻塞,讀鎖數(shù)量是否小于閾值,設置狀態(tài)比較是否成功。
如果當前不存在讀取鎖,則設置第一個讀取線程的 firstReader 和 firstReaderHoldCount。如果當前線程是第一個讀取線程,則 firstReaderHoldCount 值遞增。否則,設置當前線程對應的HoldCounter對象的值。更新成功后,當前線程的重入計數(shù)記錄在當前線程副本中的firstReaderHoldCount的readHolds(ThreadLocal類型)中。這是為了實現(xiàn)JDK 1.6中新增的getReadHoldCount()方法。該方法可以獲得當前線程重新進入共享鎖的次數(shù)。即狀態(tài)中記錄了多個線程的總重入次數(shù)。
引入這個方法讓代碼復雜了很多,但是原理還是很簡單的:如果只有一個線程,則不需要使用ThreadLocal,直接將重入計數(shù)存儲在firstReaderHoldCount成員變量中即可。當另一個線程發(fā)生時,需要使用ThreadLocal變量,readHolds。每個線程都有自己的副本,用來保存自己的重入計數(shù)。
下面是獲取讀鎖的源碼:
/** * 獲取讀鎖 * Acquires the read lock. * 如果寫鎖未被其他線程持有,執(zhí)行CAS操作更新status值,獲取讀鎖后立即返回 * <p>Acquires the read lock if the write lock is not held by * another thread and returns immediately. * * 如果寫鎖被其他線程持有,那么停止該線程的CPU調度并進入休眠狀態(tài),直到該讀鎖被釋放 * <p>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成功,則直接返回,代表搶鎖成功 * 否則,將會進入同步隊列等待,不斷執(zhí)行"tryAcquireShared"方法嘗試CAS更新status狀態(tài),直到成功搶到鎖 * 其中"tryAcquireShared"方法在NonfairSync(公平鎖)中和FairSync(非公平鎖)中都有各自的實現(xiàn) * (看這注釋是不是和寫鎖很對稱) * 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,如果不需要阻塞, * 則執(zhí)行CAS操作更新state和重入計數(shù)。 * 這里要注意的是,上面的步驟不檢查是否可重入(因為讀鎖屬于共享鎖,天生支持可重入) * 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失敗或者重入計數(shù)超過最大值導致步驟2執(zhí)行失敗 * 那就進入到fullTryAcquireShared方法進行死循環(huán),直到搶鎖成功 * 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(); //獲取該讀寫鎖狀態(tài) int c = getState(); //如果有線程獲取到了寫鎖 ,且獲取寫鎖的不是當前線程則返回失敗 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; //獲取讀鎖的重入計數(shù) int r = sharedCount(c); //如果讀線程不應該被阻塞,且重入計數(shù)小于最大值,且CAS執(zhí)行讀鎖重入計數(shù)+1成功,則執(zhí)行線程重入的計數(shù)加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的重入計數(shù)變量firstReaderHoldCount加1 firstReaderHoldCount++; } else { //否則說明有至少兩個線程共享讀鎖,獲取共享鎖重入計數(shù)器HoldCounter //從HoldCounter中拿到當前線程的線程變量cachedHoldCounter,將此線程的重入計數(shù)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條件有一個都不滿足,則進入到這個方法里進行死循環(huán)重新獲取 return fullTryAcquireShared(current); } /** * 用于處理CAS操作state失敗和tryAcquireShared中未執(zhí)行獲取可重入鎖動作的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與重試和延遲讀取保持計數(shù)之間的復雜判斷 * 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; //死循環(huán) for (;;) { //獲取讀寫鎖狀態(tài) 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; } } /** *下面是既沒有線程獲取寫鎖,當前線程又不需要阻塞的情況 */ //重入次數(shù)等于最大重入次數(shù),拋異常 if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); //如果執(zhí)行CAS操作成功將讀寫鎖的重入計數(shù)加1,則對當前持有這個共享讀鎖的線程的重入計數(shù)加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)) {//嘗試釋放一次共享鎖計數(shù) doReleaseShared();//真正釋放鎖 return true; } return false; } /** *此方法表示讀鎖線程釋放鎖。 *首先判斷當前線程是否為第一個讀線程firstReader, *若是,則判斷第一個讀線程占有的資源數(shù)firstReaderHoldCount是否為1, 若是,則設置第一個讀線程firstReader為空,否則,將第一個讀線程占有的資源數(shù)firstReaderHoldCount減1; 若當前線程不是第一個讀線程, 那么首先會獲取緩存計數(shù)器(上一個讀鎖線程對應的計數(shù)器 ), 若計數(shù)器為空或者tid不等于當前線程的tid值,則獲取當前線程的計數(shù)器, 如果計數(shù)器的計數(shù)count小于等于1,則移除當前線程對應的計數(shù)器, 如果計數(shù)器的計數(shù)count小于等于0,則拋出異常,之后再減少計數(shù)即可。 無論何種情況,都會進入死循環(huán),該循環(huán)可以確保成功設置狀態(tài)state */ protected final boolean tryReleaseShared(int unused) { // 獲取當前線程 Thread current = Thread.currentThread(); if (firstReader == current) { // 當前線程為第一個讀線程 // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) // 讀線程占用的資源數(shù)為1 firstReader = null; else // 減少占用的資源 firstReaderHoldCount--; } else { // 當前線程不為第一個讀線程 // 獲取緩存的計數(shù)器 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) // 計數(shù)器為空或者計數(shù)器的tid不為當前正在運行的線程的tid // 獲取當前線程對應的計數(shù)器 rh = readHolds.get(); // 獲取計數(shù) int count = rh.count; if (count <= 1) { // 計數(shù)小于等于1 // 移除 readHolds.remove(); if (count <= 0) // 計數(shù)小于等于0,拋出異常 throw unmatchedUnlockException(); } // 減少計數(shù) --rh.count; } for (;;) { // 死循環(huán) // 獲取狀態(tài) int c = getState(); // 獲取狀態(tài) 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; } }
通過分析可以看出,當一個線程持有讀鎖時,這個線程是無法獲取到寫鎖的,因為無論是否持有讀鎖,如果當前讀鎖被占用,它獲取寫鎖的嘗試都會失敗通過當前線程。
接下來,當一個線程持有寫鎖時,該線程可以繼續(xù)獲取讀鎖。在獲取讀鎖的過程中,如果寫鎖被占用,只有當前線程占用了寫鎖,才能獲取到讀鎖。
3、LongAdder
在高并發(fā)場景下,直接對Integer類型的整數(shù)進行i++并不能保證操作的原子性,導致線程安全問題。為此,我們在juc中使用了AtomicInteger,它是一個提供原子操作的Integer類。在內部,線程安全是通過 CAS 實現(xiàn)的。但是,當大量線程同時訪問一個鎖時,由于大量線程未能執(zhí)行CAS操作,就會發(fā)生自旋。
導致CPU資源消耗過大,執(zhí)行效率低下。 Doug Lea 對此并不滿意,于是他在 JDK 1.8 中對 CAS 進行了優(yōu)化,并提供了 LongAdder,它基于 CAS 段鎖的思想。
LongAdder是基于CAS和volatile實現(xiàn)的,由Unsafe提供。 LongAdder 的 Striped64 父類中維護了一個基變量和一個Cell數(shù)組。當多個線程對一個變量進行操作時,首先對這個基變量進行CAS操作。當檢測到其他線程時,將使用元胞數(shù)組。
例如,當即將更新 base 時,檢測到更多的線程。即casBase方法更新基值失敗,自動使用cell數(shù)組,每個線程對應一個cell,在每個線程中對cell進行CAS操作。
這樣就可以在多個值之間分擔單個值的更新壓力,降低單個值的“熱度”。也減少了大量線程的自旋,提高并發(fā)效率,分散并發(fā)壓力。這種段鎖需要額外的內存單元,但是在高并發(fā)場景下成本幾乎可以忽略不計。段鎖是一種杰出的優(yōu)化方法。 juc中的ConcurrentHashMap也是基于段鎖來保證讀寫的線程安全。
到此這篇關于淺談一下Java中的幾種JVM級別的鎖的文章就介紹到這了,更多相關JVM級別的鎖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
servlet之session工作原理簡介_動力節(jié)點Java學院整理
這篇文章主要介紹了servlet之session工作原理簡介,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07springboot程序啟動慢-未配置hostname的解決
這篇文章主要介紹了springboot程序啟動慢-未配置hostname的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08