Java並發包的locks包裡的鎖基本上已經介紹得差不多了,ReentrantLock重入鎖是個關鍵,在清楚的了解了同步器AQS的運行機制後,實際上再分析這些鎖就會顯得容易得多,這章節主講另外一個重要的鎖——ReentrantReadWriteLock讀寫鎖。
ReentrantLock是一個獨占鎖,也就是說只能由一個線程獲取鎖,但如果場景是線程只做讀的操作呢?這樣ReentrantLock就不是很合適,讀的線程並不需要保證其線程的安全性,任何一個線程都能去獲取鎖,只有這樣才能盡可能地保證性能和效率。ReentrantReadWriteLock就是這樣的一個鎖,在其內部分為讀鎖和寫鎖,可以有N個讀操作線程獲取到寫鎖,但是只能有1個寫操作線程獲取到寫鎖,那麼可以預見的是寫鎖是共享鎖(AQS中的共享模式),讀鎖是獨占鎖(AQS中的獨占模式)。首先來看讀寫鎖的接口類:
public interface ReadWriteLock { Lock readLock(); //獲取讀鎖 Lock writeLock(); //獲取寫鎖 }
可以看到ReadWriteLock接口只定義了兩個方法,獲取讀鎖和獲取寫鎖的方法。下面是ReadWriteLock的實現類——ReentrantReadWriteLock。
和ReentrantLock類似,ReentrantReadWriteLock在其內部也是通過一個內部類Sync實現同步器AQS,同樣也是通過實現Sync實現公平鎖和非公平鎖,這一點的思路和ReentrantLock類似。在ReadWriteLock接口中獲取的讀鎖和寫鎖是怎麼實現的呢?
//ReentrantReadWriteLock private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; public ReentrantReadWriteLock(){ this(false); //默認非公平鎖 } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); //鎖類型(公平/非公平) readerLock = new ReadLock(this); //構造讀鎖 writerLock = new WriteLock(this); //構造寫鎖 } …… public ReentrantReadWriteLock.WriteLock writeLock0{return writerLock;} public ReentrantReadWriteLock.ReadLock readLock0{return ReaderLock;}
//ReentrantReadWriteLock$ReadLock public static class ReadLock implements Lock { protected ReadLock(ReentrantReadwritLock lock) { sync = lock.sync; //最後還是通過Sync內部類實現鎖 } …… //它實現的是Lock接口,其余的實現可以和ReentrantLock作對比,獲取鎖、釋放鎖等等 }
//ReentrantReadWriteLock$WriteLock public static class WriteLock implemnts Lock { protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; } …… //它實現的是Lock接口,其余的實現可以和ReentrantLock作對比,獲取鎖、釋放鎖等等 }
上面是對ReentrantReadWriteLock做了一個大致的介紹,可以看到在其內部有好幾個內部類,實際上讀寫鎖內有兩個鎖——ReadLock、WriteLock,這兩個鎖都是實現自Lock接口,可以和ReentrantLock對比,而這兩個鎖的內部實現則是通過Sync,也就是同步器AQS實現的,這也可以和ReentrantLock中的Sync對比。
回顧一下AQS,其內部有兩個重要的數據結構——一個是同步隊列、一個則是同步狀態,這個同步狀態應用到讀寫鎖中也就是讀寫狀態,但AQS中只有一個state整型來表示同步狀態,讀寫鎖中則有讀、寫兩個同步狀態需要記錄。所以,讀寫鎖將AQS中的state整型做了一下處理,它是一個int型變量一共4個字節32位,那麼可以讀寫狀態就可以各占16位——高16位表示讀,低16位表示寫。
現在有一個疑問如果state的值位5,二進制為(00000000000000000000000000000101),如何快速確定讀和寫各自的狀態呢?這就要用到位移運算了。計算方式為:寫狀態state & 0x0000FFFF,讀狀態state >>> 16。寫狀態增加1等於state + 1,讀狀態增加1等於state + (1 << 16)。有關移位運算可以參考《<<、>>、>>>移位操作》。
寫鎖的獲取與釋放
根據我們之前的經驗可以得知:AQS已經將獲取鎖的算法骨架搭好了,只需子類實現tryAcquire(獨占鎖),故我們只需查看tryAcquire。
//ReentrantReadWriteLock$Sync protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread; int c = getState(); //獲取state狀態 int w = exclusiveCount(c); //獲取寫狀態,即 state & 0x00001111 if (c != 0) { //存在同步狀態(讀或寫),作下一步判斷 if (w == 0 || current != getExclusiveOwnerThread()) //寫狀態為0,但同步狀態不為0表示有讀狀態,此時獲取鎖失敗,或者當前已經有其他寫線程獲取了鎖此時也獲取鎖失敗 return false; if (w + exclusiveCount(acquire) > MAX_COUNT) //鎖重入是否超過限制 throw new Error(“Maxium lock count exceeded”); setState(c + acquire); //記錄鎖狀態 return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; //writerShouldBlock對於非公平鎖總是返回false,對於公平鎖則判斷同步隊列中是否有前驅節點 setExclusiveOwnerThread(current); return true; }
上面是寫鎖的狀態獲取,不好理解的是writerShouldBlock方法,此方法上面有描述,非公平鎖直接返回false,而對於公平鎖則是調用hasQueuedPredecessors方法如下:
//ReentrantReadWriteLock$FairSync final boolean writerShouldBlock() { return hasQueuedPredecessors(); }
原因是為什麼呢?這就要回到非公平鎖和公平鎖的區別上來了,簡單回顧一下,詳情可參考《5.Lock接口及其實現ReentrantLock》。對於非公平鎖,每次線程獲取鎖時首先會強行進行鎖獲取操作而不管同步隊列中是否有線程,當獲取不到時才會將線程構造至隊尾;對於公平鎖來講,只要同步隊列中存在線程,就不會去獲取鎖,而是將線程構造添加至隊尾。所以重新回到寫狀態的獲取上,tryAcquire方法裡,前面發現沒有線程持有鎖,但是此時會根據鎖的不同做相應操作,對於非公平鎖——搶鎖,對公平鎖——同步隊列中有線程,不搶鎖,添加至隊尾排隊。
寫鎖的釋放與ReentrantLock的釋放過程基本類似,畢竟都是獨占鎖,每次釋放減少寫的狀態,直到減小到0就表示寫鎖已經完全釋放。
讀鎖的獲取與釋放
同理,根據我們之前的經驗可以得知:AQS已經將獲取鎖的算法骨架搭好了,只需子類實現tryAcquireShared(共享鎖),故我們只需查看tryAcquireShared。我們知道對於共享模式下的鎖,它能夠被多個線程同時獲取,現在問題來了,T1線程獲取了鎖,同步狀態state=1,此時T2也獲取了鎖,state=2,接著T1線程重入state=3,也就是說讀狀態是所有線程讀鎖次數的總和,而每個線程各自獲取讀鎖的次數只能選擇保存在ThreadLock中,由線程自身維護,所以在這個地方要做一些復雜處理,源碼有點長,但復雜就在於每個線程保存自身獲取讀鎖的次數,具體參照源碼的tryAcquireShared,仔細閱讀並結合上面對寫鎖獲取的分析不難讀懂。
讀鎖的釋放值得注意的地方在於自身維護的獲取鎖的次數,以及通過移位操作減少狀態state – (1 << 16)。
以上這篇ReadWriteLock接口及其實現ReentrantReadWriteLock方法就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持。