Java多線程讀寫鎖ReentrantReadWriteLock類詳解
真實的多線程業務開發中,最常用到的邏輯就是數據的讀寫,ReentrantLock雖然具有完全互斥排他的效果(即同一時間隻有一個線程正在執行lock後面的任務),這樣做雖然保證瞭實例變量的線程安全性,但效率卻是非常低下的。所以在JDK中提供瞭一種讀寫鎖ReentrantReadWriteLock類,使用它可以加快運行效率。
讀寫鎖表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;另一個是寫操作相關的鎖,稱為排他鎖。
下面我們通過代碼去驗證下讀寫鎖之間的互斥性
ReentrantReadWriteLock
讀讀共享
首先創建一個對象,分別定義一個加讀鎖方法和一個加寫鎖的方法,
public class MyDomain3 { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void testReadLock() { try { lock.readLock().lock(); System.out.println(System.currentTimeMillis() + " 獲取讀鎖"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } public void testWriteLock() { try { lock.writeLock().lock(); System.out.println(System.currentTimeMillis() + " 獲取寫鎖"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } }
創建線程類1 調用加讀鎖方法
public class Mythread3_1 extends Thread { private MyDomain3 myDomain3; public Mythread3_1(MyDomain3 myDomain3) { this.myDomain3 = myDomain3; } @Override public void run() { myDomain3.testReadLock(); } }
@Test public void test3() throws InterruptedException { MyDomain3 myDomain3 = new MyDomain3(); Mythread3_1 readLock = new Mythread3_1(myDomain3); Mythread3_1 readLock2 = new Mythread3_1(myDomain3); readLock.start(); readLock2.start(); Thread.sleep(3000); }
執行結果:
1639621812838 獲取讀鎖 1639621812839 獲取讀鎖
可以看出兩個讀鎖幾乎同時執行,說明讀和讀之間是共享的,因為讀操作不會有線程安全問題。
寫寫互斥
創建線程類2,調用加寫鎖方法
public class Mythread3_2 extends Thread { private MyDomain3 myDomain3; public Mythread3_2(MyDomain3 myDomain3) { this.myDomain3 = myDomain3; } @Override public void run() { myDomain3.testWriteLock(); } }
@Test public void test3() throws InterruptedException { MyDomain3 myDomain3 = new MyDomain3(); Mythread3_2 writeLock = new Mythread3_2(myDomain3); Mythread3_2 writeLock2 = new Mythread3_2(myDomain3); writeLock.start(); writeLock2.start(); Thread.sleep(3000); }
執行結果:
1639622063226 獲取寫鎖 1639622064226 獲取寫鎖
從時間上看,間隔是1000ms即1s,說明寫鎖和寫鎖之間互斥。
讀寫互斥
再用線程1和線程2分別調用讀鎖與寫鎖
@Test public void test3() throws InterruptedException { MyDomain3 myDomain3 = new MyDomain3(); Mythread3_1 readLock = new Mythread3_1(myDomain3); Mythread3_2 writeLock = new Mythread3_2(myDomain3); readLock.start(); writeLock.start(); Thread.sleep(3000); }
執行結果:
1639622338402 獲取讀鎖 1639622339402 獲取寫鎖
從時間上看,間隔是1000ms即1s,和代碼裡面是一致的,證明瞭讀和寫之間是互斥的。
註意一下,”讀和寫互斥”和”寫和讀互斥”是兩種不同的場景,但是證明方式和結論是一致的,所以就不證明瞭。
最終測試結果下:
- 1、讀和讀之間不互斥,因為讀操作不會有線程安全問題
- 2、寫和寫之間互斥,避免一個寫操作影響另外一個寫操作,引發線程安全問題
- 3、讀和寫之間互斥,避免讀操作的時候寫操作修改瞭內容,引發線程安全問題
總結起來就是,多個Thread可以同時進行讀取操作,但是同一時刻隻允許一個Thread進行寫入操作。
源碼分析
讀寫鎖中的Sync也是同樣實現瞭AQS,回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個線程重復獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵。
讀寫鎖將變量切分成瞭兩個部分,高16位表示讀,低16位表示寫
當前同步狀態表示一個線程已經獲取瞭寫鎖,且重進入瞭兩次,同時也連續獲取瞭兩次讀鎖。讀寫鎖是如何迅速確定讀和寫各自的狀態呢?
static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; /** Returns the number of shared holds represented in count */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
其實是通過位運算。假設當前同步狀態值為c,寫狀態等於c & EXCLUSIVE_MASK (c&0x0000FFFF(將高16位全部抹去)),讀狀態等於c>>>16(無符號補0右移16位)。當寫狀態增加1時,等於c+1,當讀狀態增加1時,等於c+(1<<16),也就是c+0x00010000。
根據狀態的劃分能得出一個推論:c不等於0時,當寫狀態(c & 0x0000FFFF)等於0時,則讀狀態(c>>>16)大於0,即讀鎖已被獲取。
寫鎖的獲取與釋放
通過上面的測試,我們知道寫鎖是一個支持重入的排它鎖,看下源碼是如何實現寫鎖的獲取
protected final boolean tryAcquire(int acquires) { /* * Walkthrough: * 1. If read count nonzero or write count nonzero * and owner is a different thread, fail. * 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); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
第3行到第11行,簡單說瞭下整個方法的實現邏輯,這裡要誇一下,這段註釋就很容易的讓人知道代碼的功能。下面我們分析一下,第13到第15行,分別拿到瞭當前線程對象current,lock的加鎖狀態值c 以及寫鎖的值w,c!=0 表明 當前處於有鎖狀態,再繼續分析第16行到25行,有個關鍵的Note:(Note: if c != 0 and w == 0 then shared count != 0):簡單說就是:如果一個有鎖狀態但是沒有寫鎖,那麼肯定加瞭讀鎖。
第18行if條件,就是判斷加瞭讀鎖,但是當前線程不是鎖擁有的線程,那麼獲取鎖失敗,證明讀寫鎖互斥。
第20行到第25行,走到這步,說明 w !=0 ,已經獲取瞭寫鎖,隻要不超過寫鎖最大值,那麼增加寫狀態然後就可以成功獲取寫鎖。
如果代碼走到第26行,說明c==0,當前沒有加任何鎖,先執行 writerShouldBlock()方法,此方法用來判斷寫鎖是否應該阻塞,這塊是對公平與非公平鎖會有不同的邏輯,對於非公平鎖,直接返回false,不需要阻塞,下面是公平鎖執行的判斷
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
對於公平鎖需要判斷當前等待隊列中是否存在 等於當前線程並且正在排隊等待獲取鎖的線程。
寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態為0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。
讀鎖的獲取與釋放
讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取。JDK源碼如下:
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { 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; } return fullTryAcquireShared(current); }
第4行到第6行,如果寫鎖被其他線程持有,則直接返回false,獲取讀鎖失敗,證明不同線程間寫讀互斥。
第8行,readerShouldBlock() 獲取讀鎖是否應該阻塞,這兒也同樣要區分公平鎖和非公平鎖,公平鎖模式需要判斷當前等待隊列中是否存在 等於當前線程並且正在排隊等待獲取鎖的線程,存在則獲取讀鎖需要等待。
非公平鎖模式需要判斷當前等待隊列中第一個是等待寫鎖的,則方法返回true,獲取讀鎖需要等待。
fullTryAcquireShared() 主要是處理讀鎖獲取的完整版本,它處理tryAcquireShared()中沒有處理的CAS錯誤和可重入讀鎖的處理邏輯。
參考文獻
1:《Java並發編程的藝術》
2:《Java多線程編程核心技術》
到此這篇關於Java多線程讀寫鎖ReentrantReadWriteLock類詳解的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。
推薦閱讀:
- 一文瞭解Java讀寫鎖ReentrantReadWriteLock的使用
- Java 讀寫鎖源碼分析
- ReentrantReadWriteLock不能鎖升級的原因總結
- 詳解java中各類鎖的機制
- 詳解Java ReentrantReadWriteLock讀寫鎖的原理與實現