並發庫應用之五 & ReadWriteLock場景應用


  Lock比傳統線程模型中的synchronized方式更加面向對象,與生活中的鎖類似,鎖本身也應該是一個對象。兩個線程執行的代碼片段要實現同步互斥的效果,它們必須用同一個Lock對象。
  讀寫鎖:分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm自己控制的,我們只要上好相應的鎖即可。如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
讀寫鎖接口:ReadWriteLock,它的具體實現類為:ReentrantReadWriteLock
       在多線程的環境下,對同一份數據進行讀寫,會涉及到線程安全的問題。比如在一個線程讀取數據的時候,另外一個線程在寫數據,而導致前后數據的不一致性;一個線程在寫數據的時候,另一個線程也在寫,同樣也會導致線程前后看到的數據的不一致性。
       這時候可以在讀寫方法中加入互斥鎖,任何時候只能允許一個線程的一個讀或寫操作,而不允許其他線程的讀或寫操作,這樣是可以解決這樣以上的問題,但是效率卻大打折扣了。因為在真實的業務場景中,一份數據,讀取數據的操作次數通常高於寫入數據的操作,而線程與線程間的讀讀操作是不涉及到線程安全的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期間上鎖就行了。
對於以上這種情況,讀寫鎖是最好的解決方案!其中它的實現類:ReentrantReadWriteLock--顧名思義是可重入的讀寫鎖,允許多個讀線程獲得ReadLock,但只允許一個寫線程獲得WriteLock
讀寫鎖的機制:
   "讀-讀" 不互斥
   "讀-寫" 互斥
   "寫-寫" 互斥
 
ReentrantReadWriteLock會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖。
  線程進入讀鎖的前提條件:
        1. 沒有其他線程的寫鎖
    2. 沒有寫請求,或者有寫請求但調用線程和持有鎖的線程是同一個線程
  進入寫鎖的前提條件:
    1. 沒有其他線程的讀鎖
    2. 沒有其他線程的寫鎖
 
需要提前了解的概念:

  鎖降級:從寫鎖變成讀鎖;

  鎖升級:從讀鎖變成寫鎖。

  讀鎖是可以被多線程共享的,寫鎖是單線程獨占的。也就是說寫鎖的並發限制比讀鎖高,這可能就是升級/降級名稱的來源。

  如下代碼會產生死鎖,因為同一個線程中,在沒有釋放讀鎖的情況下,就去申請寫鎖,這屬於鎖升級,ReentrantReadWriteLock是不支持的。

 ReadWriteLock rtLock = new ReentrantReadWriteLock();
 rtLock.readLock().lock();
 System.out.println("get readLock.");
 rtLock.writeLock().lock();
 System.out.println("blocking");

  ReentrantReadWriteLock支持鎖降級,如下代碼不會產生死鎖。

ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");

rtLock.readLock().lock();
System.out.println("get read lock");

  以上這段代碼雖然不會導致死鎖,但沒有正確的釋放鎖。從寫鎖降級成讀鎖,並不會自動釋放當前線程獲取的寫鎖,仍然需要顯示的釋放,否則別的線程永遠也獲取不到寫鎖。

 
============以下我會通過一個真實場景下的緩存機制來講解 ReentrantReadWriteLock 實際應用============
首先來看看ReentrantReadWriteLock的javaodoc文檔中提供給我們的一個很好的Cache實例代碼案例:
 1 class CachedData {
 2   Object data;
 3   volatile boolean cacheValid;
 4   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 5 
 6   public void processCachedData() {
 7     rwl.readLock().lock();
 8     if (!cacheValid) {
 9       // Must release read lock before acquiring write lock
10       rwl.readLock().unlock();
11       rwl.writeLock().lock();
12       try {
13         // Recheck state because another thread might have,acquired write lock and changed state before we did.
14         if (!cacheValid) {
15           data = ...
16           cacheValid = true;
17         }
18         // 在釋放寫鎖之前通過獲取讀鎖降級寫鎖(注意此時還沒有釋放寫鎖)
19         rwl.readLock().lock();
20       } finally {
21         rwl.writeLock().unlock(); // 釋放寫鎖而此時已經持有讀鎖
22       }
23     }
24 
25     try {
26       use(data);
27     } finally {
28       rwl.readLock().unlock();
29     }
30   }
31 }

以上代碼加鎖的順序為:

  1. rwl.readLock().lock();

  2. rwl.readLock().unlock();

  3. rwl.writeLock().lock();

  4. rwl.readLock().lock();

  5. rwl.writeLock().unlock();

  6. rwl.readLock().unlock();

以上過程整體講解:

1. 多個線程同時訪問該緩存對象時,都加上當前對象的讀鎖,之后其中某個線程優先查看data數據是否為空。【加鎖順序序號:1 】

2. 當前查看的線程發現沒有值則釋放讀鎖立即加上寫鎖,准備寫入緩存數據。(不明白為什么釋放讀鎖的話可以查看上面講解進入寫鎖的前提條件)【加鎖順序序號:2和3 】

3. 為什么還會再次判斷是否為空值(!cacheValid)是因為第二個、第三個線程獲得讀的權利時也是需要判斷是否為空,否則會重復寫入數據。

4. 寫入數據后先進行讀鎖的降級后再釋放寫鎖。【加鎖順序序號:4和5 】

5. 最后數據數據返回前釋放最終的讀鎖。【加鎖順序序號:6 】

  如果不使用鎖降級功能,如先釋放寫鎖,然后獲得讀鎖,在這個get過程中,可能會有其他線程競爭到寫鎖 或者是更新數據 則獲得的數據是其他線程更新的數據,可能會造成數據的污染,即產生臟讀的問題。

下面,讓我們來實現真正趨於實際生產環境中的緩存案例:

 1 import java.util.HashMap;
 2 import java.util.Map;
 3 import java.util.concurrent.locks.ReadWriteLock;
 4 import java.util.concurrent.locks.ReentrantReadWriteLock;
 5 
 6 public class CacheDemo {
 7     /**
 8      * 緩存器,這里假設需要存儲1000左右個緩存對象,按照默認的負載因子0.75,則容量=750,大概估計每一個節點鏈表長度為5個
 9      * 那么數組長度大概為:150,又有雨設置map大小一般為2的指數,則最近的數字為:128
10      */
11     private Map<String, Object> map = new HashMap<>(128);
12     private ReadWriteLock rwl = new ReentrantReadWriteLock();
13     public static void main(String[] args) {
14 
15     }
16     public Object get(String id){
17         Object value = null;
18         rwl.readLock().lock();//首先開啟讀鎖,從緩存中去取
19         try{
if(map.get(id) == null){ //如果緩存中沒有釋放讀鎖,上寫鎖 22 rwl.readLock().unlock(); 23 rwl.writeLock().lock(); 24 try{ 25 if(value == null){ //防止多寫線程重復查詢賦值 26 value = "redis-value"; //此時可以去數據庫中查找,這里簡單的模擬一下 27 } 28 rwl.readLock().lock(); //加讀鎖降級寫鎖,不明白的可以查看上面鎖降級的原理與保持讀取數據原子性的講解 29 }finally{ 30 rwl.writeLock().unlock(); //釋放寫鎖 31 } 32 } 33 }finally{ 34 rwl.readLock().unlock(); //最后釋放讀鎖 35 } 36 return value; 37 } 38 }

 提示:讀寫鎖之后有一個與它配合使用的有條件的阻塞,可以實現線程間的通信,它就是Condition。具體詳情請查看我的博客:並發庫應用之六 & 有條件阻塞Condition應用

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM