有了CopyOnWrite為何又要有ReadWriteLock?
引言
前文我們有介紹《看了CopyOnWriteArrayList後自己實現了一個CopyOnWriteHashMap》 關於CopyOnWrite
容器的,但是它也有一些缺點:
-
記憶體佔用問題:因為
CopyOnWrite
的寫時複製機制每次進行寫操作的時候都會有兩個數組對象的記憶體,如果這個數組對象佔用的記憶體較大的話,如果頻繁的進行寫入就會造成頻繁的Yong GC
和Full GC
-
數據一致性問題:
CopyOnWrite
容器只能保證數據的最終一致性,不能保證數據的實時一致性。讀操作的執行緒可能不會立即讀取到新修改的數據,因為修改操作發生在副本上。但最終修改操作會完成並更新容器所以這是最終一致性。當時有說到解決這兩個缺點我們可以使用Collections.synchronizedList()
來替代,找個無非就是對list的增刪改查方法都加了synchronized實現。我們知道synchronized
其實是一個獨佔鎖 (排他鎖),如果不知道什麼是獨佔鎖的可以看看這個文章《史上最全 Java 中各種鎖的介紹》 裡面基本上把java裡面的鎖都介紹完了。但是這樣的話就會存在一個性能問題,如果對於讀多寫少的場景,每次讀也要去獲取鎖,讀完了之後再釋放鎖,這樣就造成了每個讀的請求都要進行獲取鎖,但是讀的話並不會引起數據不安全,這樣就會造成一個性能瓶頸。為了解決這個問題,就又出現了一種新的鎖,讀寫鎖(ReadWriteLock
)。
什麼是讀寫鎖
根據名字我們也可以猜個大概,就是有兩把鎖,分別是讀鎖和寫鎖。讀鎖在同一時刻可以允許多個讀執行緒獲取,但是在寫執行緒訪問的時候,所有的讀執行緒和其他寫執行緒都會被阻塞。寫鎖同一時刻只能有一個寫執行緒獲取成功,其他都會被阻塞。讀寫鎖實際維護了兩把鎖,一個讀鎖和一個寫鎖,通過讀鎖和寫鎖進行區分,在讀多寫少的情況下並發性比獨佔鎖有了很大的提升。在java裡面對讀寫鎖的實現就是ReentrantReadWriteLock
,它有以下特性:
★
公平性選擇:支援非公平性(默認)和公平的鎖獲取方式,吞吐量還是非公平優於公平;
重入性:支援重入,讀鎖獲取後能再次獲取,寫鎖獲取之後能夠再次獲取寫鎖,同時也能夠獲取讀鎖;
鎖降級:遵循獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖
」
ReentrantReadWriteLock 的使用
我們先從官網來個事例//docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html,看看它是如何使用的
`class RWDictionary {`
`private final Map<String, Data> m = new TreeMap<String, Data>();`
`private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();`
`private final Lock r = rwl.readLock();`
`private final Lock w = rwl.writeLock();`
`public Data get(String key) {`
`r.lock();`
`try { return m.get(key); }`
`finally { r.unlock(); }`
`}`
`public String[] allKeys() {`
`r.lock();`
`try { return m.keySet().toArray(); }`
`finally { r.unlock(); }`
`}`
`public Data put(String key, Data value) {`
`w.lock();`
`try { return m.put(key, value); }`
`finally { w.unlock(); }`
`}`
`public void clear() {`
`w.lock();`
`try { m.clear(); }`
`finally { w.unlock(); }`
`}`
`}`
這個使用起來還是非常簡單明了的,跟ReentrantLock
的用法基本一致,寫的時候獲取寫鎖,寫完了釋放寫鎖,讀的時候獲取讀鎖,讀完了就釋放讀寫。
讀寫鎖的實現分析
我們知道ReentrantLock
是通過state
來控制鎖的狀態,以及前面所介紹的《Java高並發編程基礎三大利器之Semaphore》《Java高並發編程基礎三大利器之CountDownLatch》《Java高並發編程基礎三大利器之CyclicBarrier》 都是通過state
來進行實現的那ReentrantReadWriteLock毋庸置疑肯定也是通過AQS
的state
來實現的,不過state
是一個int
值它是如何來讀鎖和寫鎖的。
讀寫鎖狀態的實現分析
如果我們有看過執行緒池的源碼,我們知道執行緒池的狀態和執行緒數是通過一個int
類型原子變數(高3
位保存運行狀態,低29
位保存執行緒數)來控制的。同樣的ReentrantReadWriteLock
也是通過一個state
的高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; }`
-
sharedCount
: 讀鎖數量 是將同步狀態(int c
)無符號右移16
位,即取同步狀態的高16
位。 -
exclusiveCount
:寫鎖數量 我們要看下EXCLUSIVE_MASK
這個靜態變數:它是1進行左移16位然後減1也就是0X0000FFFF
即(1 << SHARED_SHIFT) - 1= 0X0000FFFF
所以exclusiveCount
就是相當於c&0X0000FFFF
所以也就是低16位用來表示寫鎖的獲取次數。
源碼分析
基於jdk1.8 既然ReentrantReadWriteLock
也是基於AQS
來實現的,那麼它肯定是重寫了AQS
的獲取鎖的方法,那我們就直接去ReentrantReadWriteLock
這個類裡面看看lock
的地方我們先看看獲取讀鎖的地方
`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)`
`// 當前狀態不為0,但是寫鎖為0 就說明讀鎖不為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;`
`}`
`//writerShouldBlock 公平鎖和非公平鎖的判斷`
`if (writerShouldBlock() ||`
`!compareAndSetState(c, c + acquires))`
`return false;`
`setExclusiveOwnerThread(current);`
`return true;`
`}`
寫鎖完了,接下來肯定就是讀鎖了由於讀鎖是共享鎖,所以也應該重寫了tryAcquireShared
這個就不貼程式碼了,和讀鎖差不多這個就不做分析了。其實把AQS
弄明白了再來看這些基於AQS
來實現的玩意還是比較容易的。
讀寫鎖的升級與降級
前面我們有提到讀寫鎖是可以降級的,但是沒有說是否可以升級。我們先看看什麼是鎖降級和鎖升級
-
鎖降級:從寫鎖變成讀鎖;它的過程是先持有寫鎖,在獲取讀鎖,再釋放寫鎖。如果是持有寫鎖,釋放寫鎖,再獲取讀鎖這種情況不是鎖降級。
-
為什麼要鎖降級?
★
主要是為了保證數據的可見性,如果當前執行緒不獲取讀鎖而是直接釋放寫鎖, 假設此刻另一個執行緒(記作執行緒T)獲取了寫鎖並修改了數據,那麼當前執行緒無法感知執行緒T的數據更新。如果當前執行緒獲取讀鎖,即遵循鎖降級的步驟,則執行緒T將會被阻塞,直到當前執行緒使用數據並釋放讀鎖之後,執行緒T才能獲取寫鎖進行數據更新。來源於《Java 並發編程的藝術》
」
- 鎖升級:從讀鎖變成寫鎖。先持有讀鎖,再去獲取寫鎖(這是不會成功的)因為獲取寫鎖是獨佔鎖,如果有讀鎖被佔用了,寫鎖就會放入隊列中等待,直至讀鎖全部被釋放之後才有可能獲取到寫鎖。
思考題
-
本篇文章主要介紹了單機情況的讀寫鎖,如果要實現一個分散式的讀寫鎖該如何實現?
-
ReentrantReadWriteLock
的飢餓問題如何解決?(ReentrantReadWriteLock實現了讀寫分離,想要獲取讀鎖就必須確保當前沒有其他任何讀寫鎖了,但是一旦讀操作比較多的時候,想要獲取寫鎖就變得比較困難了,因為當前有可能會一直存在讀鎖。而無法獲得寫鎖。)
結束
-
由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
-
如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
-
感謝您的閱讀,十分歡迎並感謝您的關注。
-
站在巨人的肩膀上摘蘋果:
《並發編程的藝術》
往期精選
推薦👍 :Java高並發編程基礎三大利器之CyclicBarrier
推薦👍 :Java高並發編程基礎三大利器之CountDownLatch
推薦👍 :Java高並發編程基礎三大利器之Semaphore
推薦👍 :Java高並發編程基礎之AQS
推薦👍 :可惡的爬蟲直接把生產6台機器爬掛了!