ReadWriteLock場景應用解析

本人免費整理了Java高級資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G,須要本身領取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

 

Lock比傳統線程模型中的synchronized方式更加面向對象,與生活中的鎖相似,鎖自己也應該是一個對象。兩個線程執行的代碼片斷要實現同步互斥的效果,它們必須用同一個Lock對象。java

讀寫鎖:分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm本身控制的,咱們只要上好相應的鎖便可。若是你的代碼只讀數據,能夠不少人同時讀,但不能同時寫,那就上讀鎖;若是你的代碼修改數據,只能有一我的在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!redis

 

讀寫鎖接口:ReadWriteLock,它的具體實現類爲:ReentrantReadWriteLock數據庫

在多線程的環境下,對同一份數據進行讀寫,會涉及到線程安全的問題。好比在一個線程讀取數據的時候,另一個線程在寫數據,而致使先後數據的不一致性;一個線程在寫數據的時候,另外一個線程也在寫,一樣也會致使線程先後看到的數據的不一致性。數組

這時候能夠在讀寫方法中加入互斥鎖,任什麼時候候只能容許一個線程的一個讀或寫操做,而不容許其餘線程的讀或寫操做,這樣是能夠解決這樣以上的問題,可是效率卻大打折扣了。由於在真實的業務場景中,一份數據,讀取數據的操做次數一般高於寫入數據的操做,而線程與線程間的讀讀操做是不涉及到線程安全的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期間上鎖就好了。緩存

 

對於以上這種狀況,讀寫鎖是最好的解決方案!其中它的實現類:ReentrantReadWriteLock--顧名思義是可重入的讀寫鎖,容許多個讀線程得到ReadLock,但只容許一個寫線程得到WriteLock安全

讀寫鎖的機制:多線程

"讀-讀" 不互斥併發

"讀-寫" 互斥jvm

"寫-寫" 互斥分佈式

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實例代碼案例:

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

  public void processCachedData() {
    rwl.readLock().lock();
    if (!cacheValid) {
      // Must release read lock before acquiring write lock
      rwl.readLock().unlock();
      rwl.writeLock().lock();
      try {
        // Recheck state because another thread might have,acquired write lock and changed state before we did.
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 在釋放寫鎖以前經過獲取讀鎖降級寫鎖(注意此時尚未釋放寫鎖)
        rwl.readLock().lock();
      } finally {
        rwl.writeLock().unlock(); // 釋放寫鎖而此時已經持有讀鎖
      }
    }

    try {
      use(data);
    } finally {
      rwl.readLock().unlock();
    }
  }
}

 


以上代碼加鎖的順序爲:
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過程當中,可能會有其餘線程競爭到寫鎖 或者是更新數據 則得到的數據是其餘線程更新的數據,可能會形成數據的污染,即產生髒讀的問題。
下面,讓咱們來實現真正趨於實際生產環境中的緩存案例:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheDemo {
    /**
     * 緩存器,這裏假設須要存儲1000左右個緩存對象,按照默認的負載因子0.75,則容量=750,大概估計每個節點鏈表長度爲5個
     * 那麼數組長度大概爲:150,又有雨設置map大小通常爲2的指數,則最近的數字爲:128
     */
    private Map<String, Object> map = new HashMap<>(128);
    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    public static void main(String[] args) {

    }
    public Object get(String id){
        Object value = null;
        rwl.readLock().lock();//首先開啓讀鎖,從緩存中去取
        try{
               if(map.get(id) == null){  //若是緩存中沒有釋放讀鎖,上寫鎖
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try{
                    if(value == null){ //防止多寫線程重複查詢賦值
                        value = "redis-value";  //此時能夠去數據庫中查找,這裏簡單的模擬一下
                    }
                    rwl.readLock().lock(); //加讀鎖降級寫鎖,不明白的能夠查看上面鎖降級的原理與保持讀取數據原子性的講解
                }finally{
                    rwl.writeLock().unlock(); //釋放寫鎖
                }
            }
        }finally{
            rwl.readLock().unlock(); //最後釋放讀鎖
        }
        return value;
    }
}
相關文章
相關標籤/搜索