ReentrantReadWriteLock 介紹
ReentrantReadWriteLock 是 Java 中的一個讀寫鎖實現,它包含了兩種鎖:讀鎖和寫鎖。讀鎖是共享鎖,多個執行緒可以同時獲取讀鎖進行讀操作,但是寫鎖是排他鎖,只有一個執行緒可以獲取寫鎖進行寫操作。ReentrantReadWriteLock 提供了比單一鎖更高效的併發效能,特別適用於讀多寫少的場景。
JDK 提供了 ReentrantReadWriteLock 讀寫鎖,使用它可以加快效率,在某些不需要操作例項變數的方法中,完全可以使用讀寫鎖 ReemtrantReadWriteLock 來提升該方法的執行速度。
定義:讀寫鎖表示有兩個鎖,一個是讀操作相關的鎖,也稱為共享鎖;另一個是寫操作相關的鎖,也叫排他鎖。
定義解讀:也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥、寫鎖與寫鎖互斥。在沒有執行緒 Thread 進行寫入操作時,進行讀取操作的多個 Thread 都可以獲取讀鎖,而進行寫入操作的 Thread 只有在獲取寫鎖後才能進行寫入操作。即多個 Thread 可以同時進行讀取操作,但是同一時刻只允許一個 Thread 進行寫入操作。
ReentrantReadWriteLock 的主要特點包括:
- 讀寫分離:讀操作可以併發進行,寫操作是排他的。
- 可重入性:讀鎖和寫鎖都支援重入。
- 公平性:可以選擇是否公平地獲取讀寫鎖。
- 鎖降級:可以將寫鎖降級為讀鎖,但不能將讀鎖升級為寫鎖。
- 條件變數:支援 Condition 條件變數。
ReentrantReadWriteLock 的主要方法包括:
- readLock():獲取讀鎖。
- writeLock():獲取寫鎖。
- readLock().lock():獲取讀鎖。
- writeLock().lock():獲取寫鎖。
- readLock().unlock():釋放讀鎖。
- writeLock().unlock():釋放寫鎖。
ReentrantReadWriteLock 的特點
性質 1 :可重入性。
ReentrantReadWriteLock 與 ReentrantLock 以及 synchronized 一樣,都是可重入性鎖,這裡不會再多加贅述所得可重入性質,之前已經做過詳細的講解。
性質 2 :讀寫分離。
我們知道,對於一個資料,不管是幾個執行緒同時讀都不會出現任何問題,但是寫就不一樣了,幾個執行緒對同一個資料進行更改就可能會出現資料不一致的問題,因此想出了一個方法就是對資料加鎖,這時候出現了一個問題:
執行緒寫資料的時候加鎖是為了確保資料的準確性,但是執行緒讀資料的時候再加鎖就會大大降低效率,這時候怎麼辦呢?那就對寫資料和讀資料分開,加上兩把不同的鎖,不僅保證了正確性,還能提高效率。
性質 3 :可以鎖降級,寫鎖降級為讀鎖。
執行緒獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。
性質 4 :不可鎖升級。
執行緒獲取讀鎖是不能直接升級為寫入鎖的。需要釋放所有讀取鎖,才可獲取寫鎖。
public class LockExample3 { private final Map<String, Data> map = new TreeMap<>(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); public Data get(String key) { readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } public Set<String> getAllKeys() { readLock.lock(); try { return map.keySet(); } finally { readLock.unlock(); } } public Data put(String key, Data value) { writeLock.lock(); try { return map.put(key, value); } finally { readLock.unlock(); } } class Data { } }
ReentrantReadWriteLock 讀鎖共享
我們之前說過,ReentrantReadWriteLock 之所以優秀,是因為讀鎖與寫鎖是分離的,當所有的執行緒都為讀操作時,不會造成執行緒之間的互相阻塞,提升了效率,那麼接下來,我們透過程式碼例項進行學習。
場景設計:
- 建立三個執行緒,執行緒名稱分別為 t1,t2,t3,執行緒實現方式自行選擇;
- 三個執行緒同時執行獲取讀鎖,讀鎖成功後列印執行緒名和獲取結果,並沉睡 2000 毫秒,便於觀察其他執行緒是否可共享讀鎖;
- finally 模組中釋放鎖並列印執行緒名和釋放結果;
- 執行程式,觀察結果。
結果預期:三條執行緒能同時獲取鎖,因為讀鎖共享。
public class DemoTest { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 讀寫鎖 private int i; public String readI() { try { lock.readLock().lock();// 佔用讀鎖 System.out.println("threadName -> " + Thread.currentThread().getName() + " 佔用讀鎖,i->" + i); Thread.sleep(2000); } catch (InterruptedException e) { } finally { System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放讀鎖,i->" + i); lock.readLock().unlock();// 釋放讀鎖 } return i + ""; } public static void main(String[] args) { final DemoTest demo1 = new DemoTest(); Runnable runnable = new Runnable() { @Override public void run() { demo1.readI(); } }; new Thread(runnable, "t1"). start(); new Thread(runnable, "t2"). start(); new Thread(runnable, "t3"). start(); } }
ReentrantReadWriteLock 讀寫互斥
當共享變數有寫操作時,必須要對資源進行加鎖,此時如果一個執行緒正在進行讀操作,那麼寫操作的執行緒需要等待。同理,如果一個執行緒正在寫操作,讀操作的執行緒需要等待。
場景設計:細節操作不詳細闡述,看示例程式碼即可。
- 建立兩個執行緒,執行緒名稱分別為 t1,t2;
- 執行緒 t1 進行讀操作,獲取到讀鎖之後,沉睡 5000 毫秒;
- 執行緒 t2 進行寫操作;
- 開啟 t1,1000 毫秒後開啟 t2 執行緒;
- 執行程式,觀察結果。
結果預期:執行緒 t1 獲取了讀鎖,在沉睡的 5000 毫秒中,執行緒 t2 只能等待,不能獲取到鎖,因為讀寫互斥。
public class DemoTest { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 讀寫鎖 private int i; public String readI() { try { lock.readLock().lock();// 佔用讀鎖 System.out.println("threadName -> " + Thread.currentThread().getName() + " 佔用讀鎖,i->" + i); Thread.sleep(5000); } catch (InterruptedException e) { } finally { System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放讀鎖,i->" + i); lock.readLock().unlock();// 釋放讀鎖 } return i + ""; } public void addI() { try { lock.writeLock().lock();// 佔用寫鎖 System.out.println("threadName -> " + Thread.currentThread().getName() + " 佔用寫鎖,i->" + i); i++; } finally { System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放寫鎖,i->" + i); lock.writeLock().unlock();// 釋放寫鎖 } } public static void main(String[] args) throws InterruptedException { final DemoTest demo1 = new DemoTest(); new Thread(new Runnable() { @Override public void run() { demo1.readI(); } }, "t1"). start(); Thread.sleep(1000); new Thread(new Runnable() { @Override public void run() { demo1.addI(); } }, "t2"). start(); } }