JUC學習筆記(七)
1、讀寫鎖
1.1、讀寫鎖介紹
現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,多個執行緒同時讀一個資源沒有任何問題,所以應該允許多個執行緒同時讀取共享資源;但是如果一個執行緒想去寫這些共享資源,就不應該允許其他執行緒對該資源進行讀和寫的操作了。
針對這種場景,JAVA的併發包提供了讀寫鎖 ReentrantReadWriteLock, 它表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;一個是寫相關的鎖,稱為排他鎖
(1) 執行緒進入讀鎖的前提條件:
- 沒有其他執行緒的寫鎖
- 沒有寫請求, 或者有寫請求,但呼叫執行緒和持有鎖的執行緒是同一個(可重入 鎖)。
(2) 執行緒進入寫鎖的前提條件:
- 沒有其他執行緒的讀鎖
- 沒有其他執行緒的寫鎖
讀寫鎖具有以下三個重要的特性:
(1)公平選擇性:支援非公平(預設)和公平的鎖獲取方式,吞吐量還是非公平優於公平。
(2)重進入:讀鎖和寫鎖都支援執行緒重進入。
(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖。
1.2、ReentrantReadWriteLock
ReentrantReadWriteLock 類的整體結構
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { /** * 讀鎖 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** * 寫鎖 */ private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; /** * 使用預設(非公平)的排序屬性建立一個新的ReentrantReadWriteLock */ public ReentrantReadWriteLock() { this(false); } /** * 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } /** * 返回用於寫入操作的鎖 */ public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } /** * 返回用於讀取操作的鎖 */ public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } abstract static class Sync extends AbstractQueuedSynchronizer { } static final class NonfairSync extends Sync { } static final class FairSync extends Sync { } public static class ReadLock implements Lock, java.io.Serializable { } public static class WriteLock implements Lock, java.io.Serializable { } }
可以看到,ReentrantReadWriteLock 實現了 ReadWriteLock 介面,ReadWriteLock 介面定義了獲取讀鎖和寫鎖的規範,具體需要實現類去實現;同時其還實現了 Serializable 介面,表示可以進行序列化,在原始碼中可以看到 ReentrantReadWriteLock 實現了自己的序列化邏輯。
1.3、案例
場景: 使用 ReentrantReadWriteLock 對一個 hashmap 進行讀和寫操作
public class ReadWriteLockDemo { public static void main(String[] args) throws InterruptedException { MyCache myCache = new MyCache(); for (int i = 1; i <= 5; i++) { final int num = i; new Thread(() -> { myCache.put(num + "", num); }, String.valueOf(i)).start(); } for (int i = 1; i <= 5; i++) { final int num = i; new Thread(() -> { Object result = myCache.get(num + ""); }, String.valueOf(i)).start(); } } } class MyCache { private volatile Map<String, Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 放資料 public void put(String key, Object value) { Lock lock = readWriteLock.writeLock(); lock.lock(); try { System.out.println(Thread.currentThread().getName() + "正在進行寫操作:" + key); TimeUnit.MICROSECONDS.sleep(300); map.put(key, value); System.out.println(Thread.currentThread().getName() + "完成寫操作:" + key); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } // 取資料 public Object get(String key) { Lock lock = readWriteLock.readLock(); lock.lock(); try { System.out.println(Thread.currentThread().getName() + "正在進行讀操作:" + key); TimeUnit.MICROSECONDS.sleep(300); Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "完成讀操作:" + key); return result; } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } return null; } }
1.4、小結
- 線上程持有讀鎖的情況下,該執行緒不能取得寫鎖(因為獲取寫鎖的時候,如果發 現當前的讀鎖被佔用,就馬上獲取失敗,不管讀鎖是不是被當前執行緒持有)。
- 線上程持有寫鎖的情況下,該執行緒可以繼續獲取讀鎖(獲取讀鎖時如果發現寫 鎖被佔用,只有寫鎖沒有被當前執行緒佔用的情況才會獲取失敗)。
原因: 當執行緒獲取讀鎖的時候,可能有其他執行緒同時也在持有讀鎖,因此不能把 獲取讀鎖的執行緒“升級”為寫鎖;而對於獲得寫鎖的執行緒,它一定獨佔了讀寫 鎖,因此可以繼續讓它獲取讀鎖,當它同時獲取了寫鎖和讀鎖後,還可以先釋
放寫鎖繼續持有讀鎖,這樣一個寫鎖就“降級”為了讀鎖。