Java中的讀寫鎖

1、讀寫鎖

一、初識讀寫鎖

  a)Java中的鎖——Lock和synchronized中介紹的ReentrantLock和synchronized基本上都是排它鎖,意味着這些鎖在同一時刻只容許一個線程進行訪問,而讀寫鎖在同一時刻能夠容許多個讀線程訪問,在寫線程訪問的時候其餘的讀線程和寫線程都會被阻塞。讀寫鎖維護一對鎖(讀鎖和寫鎖),經過鎖的分離,使得併發性提升。html

  b)關於讀寫鎖的基本使用:在不使用讀寫鎖的時候,通常狀況下咱們須要使用synchronized搭配等待通知機制完成併發控制(寫操做開始的時候,全部晚於寫操做的讀操做都會進入等待狀態),只有寫操做完成並通知後纔會將等待的線程喚醒繼續執行。併發

  若是改用讀寫鎖實現,只須要在讀操做的時候獲取讀鎖,寫操做的時候獲取寫鎖。當寫鎖被獲取到的時候,後續操做(讀寫)都會被阻塞,只有在寫鎖釋放以後纔會執行後續操做。併發包中對ReadWriteLock接口的實現類是ReentrantReadWriteLock,這個實現類具備下面三個特色源碼分析

  ①具備與ReentrantLock相似的公平鎖和非公平鎖的實現:默認的支持非公平鎖,對於兩者而言,非公平鎖的吞吐量因爲公平鎖;post

  ②支持重入:讀線程獲取讀鎖以後可以再次獲取讀鎖,寫線程獲取寫鎖以後能再次獲取寫鎖,也能夠獲取讀鎖。ui

  ③鎖能降級:遵循獲取寫鎖、獲取讀鎖在釋放寫鎖的順序,即寫鎖可以降級爲讀鎖spa

二、讀寫鎖源碼分析

a)ReadWriteLock接口中只有兩個方法,分別是readLock和writeLock

 1 public interface ReadWriteLock {
 2     /**
 3      * 返回讀鎖
 4      */
 5     Lock readLock();
 6 
 7     /**
 8      * 返回寫鎖
 9      */
10     Lock writeLock();
11 }

b)關於讀寫讀寫狀態的設計

  ①做爲已經實現的同步組件,讀寫鎖一樣是須要實現同步器來實現同步功能,同步器的同步狀態就是讀寫鎖的讀寫狀態,只是讀寫鎖的同步器須要在同步狀態上維護多個讀線程和寫線程的狀態。使用按位切割的方式將一個整形變量按照高低16位切割成兩個部分。對比下圖,低位值表示當前獲取寫鎖的線程重入兩次,高位的值表示當前獲取讀鎖的線程重入一次。讀寫鎖的獲取伴隨着讀寫狀態值的更新。當低位爲0000_0000_0000_0000的時候表示寫鎖已經釋放,當高位爲0000_0000_0000_0000的時候表示讀鎖已經釋放。線程

  ②從下面的劃分獲得:當state值不等於0的時候,若是寫狀態(state & 0x0000FFFF)等於0的話,讀狀態是大於0的,表示讀鎖被獲取;若是寫狀態不等於0的話,讀鎖沒有被獲取。這個特色也在源碼中實現。設計

c)寫鎖writeLock

  ①上面說到過,讀寫鎖是支持重入的鎖,而對於寫鎖而言仍是排他的,這樣避免多個線程同時去修改臨界資源致使程序出現錯誤。若是當前線程已經獲取了寫鎖,則按照上面讀寫狀態的設計增長寫鎖狀態的值;若是當前線程在獲取寫鎖的時候,讀鎖已經被獲取或者該線程以前已經有別的線程獲取到寫鎖,當前線程就會進入等待狀態。3d

 1 static final int SHARED_SHIFT   = 16;
 2 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //1左移16位減1=>0000_0000_0000_0000_1111_1111_1111_1111
 3 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //返回讀狀態的值
 4 protected final boolean tryAcquire(int acquires) {
 5         /*
 6          * Walkthrough:
 7          * 1. 若是讀狀態不爲0或者寫狀態不爲0而且寫線程不是本身,返回false
 8          * 2. 若是已經超過了可重入的計數值MAX_COUNT,就會返回false
 9          * 3. 若是該線程是可重入獲取或隊列策略容許,則該線程有資格得到鎖定;同時更新全部者和寫鎖狀態值
10          */
11         Thread current = Thread.currentThread(); //獲取當前線程
12         int c = getState(); //獲取當前寫鎖狀態值
13         int w = exclusiveCount(c); //獲取寫狀態的值
14         //當同步狀態state值不等於0的時候,若是寫狀態(state & 0x0000FFFF)等於0的話,讀狀態是大於0的,表示讀鎖被獲取
15         if (c != 0) {
16             if (w == 0 || current != getExclusiveOwnerThread())
17                 return false;
18             if (w + exclusiveCount(acquires) > MAX_COUNT) //若是已經超過了可重入的計數值MAX_COUNT,就會返回false
19                 throw new Error("Maximum lock count exceeded");
20             // 重入鎖:更新狀態值
21             setState(c + acquires);
22             return true;
23         }
24         if (writerShouldBlock() ||
25             !compareAndSetState(c, c + acquires))
26             return false;
27         setExclusiveOwnerThread(current);
28         return true;
29     }

  ②分析一下上面的寫鎖獲取源碼code

  tryAcquire中線程獲取寫鎖的條件:讀鎖沒有線程獲取,寫鎖被獲取而且被獲取的線程是本身,那麼該線程能夠重入的獲取鎖,而判斷讀鎖是否被獲取的條件就是(當同步狀態state值不等於0的時候,若是寫狀態(state & 0x0000FFFF)等於0的話,讀狀態是大於0的,表示讀鎖被獲取)。對於讀寫鎖而言,須要保證寫鎖的更新結果操做對讀操做是可見的,這樣的話寫鎖的獲取就須要保證其餘的讀線程沒有獲取到讀鎖。

  ③寫鎖的釋放源碼

  寫鎖的釋放和ReentrantLock的鎖釋放思路基本相同,從源碼中能夠看出來,每次釋放都是減小寫狀態,直到寫狀態值爲0(exclusiveCount(nextc) == 0)的時候釋放寫鎖,後續阻塞等待的讀寫線程能夠繼續競爭鎖。

 1 static final int SHARED_SHIFT   = 16;
 2 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //1左移16位減1=>0000_0000_0000_0000_1111_1111_1111_1111
 3 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //返回讀狀態的值
 4 protected final boolean tryRelease(int releases) {
 5     if (!isHeldExclusively())
 6         throw new IllegalMonitorStateException();
 7     int nextc = getState() - releases;
 8     boolean free = exclusiveCount(nextc) == 0; //寫狀態值爲0,就釋放寫鎖,並將同步狀態的線程持有者置爲null,而後更新狀態值
 9     if (free)
10         setExclusiveOwnerThread(null);
11     setState(nextc);
12     return free;
13 }

d)讀鎖readLock

  ①讀鎖是一樣是支持重入的,除此以外也是共享式的,可以被多個線程獲取。在同一時刻的競爭隊列中,若是沒有寫線程想要獲取讀寫鎖,那麼讀鎖總會被讀線程獲取到(而後更新讀狀態的值)。每一個讀線程均可以重入的獲取讀鎖,而對應的獲取次數保存在本地線程中,由線程自身維護該值。

  ②獲取讀鎖的條件:其餘線程已經獲取了寫鎖,則當前線程獲取讀鎖會失敗而進入等待狀態;若是當前線程獲取了寫鎖或者寫鎖沒有被獲取,那麼就能夠獲取到讀鎖,並更細同步狀態(讀狀態值)。

  ③讀鎖的每次釋放都是減小讀狀態,

f)鎖的降級

  鎖降級的概念:若是當先線程是寫鎖的持有者,並保持得到寫鎖的狀態,同時又獲取到讀鎖,而後釋放寫鎖的過程。(注意不一樣於這樣的分段過程:當前線程擁有寫鎖,釋放掉寫鎖以後再獲取讀鎖的過程,這種分段過程不能稱爲鎖降級)。

 1 class CachedData {
 2   Object data;
 3   volatile boolean cacheValid;
 4   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 5 
 6   void processCachedData() {
 7     // 獲取讀鎖
 8     rwl.readLock().lock();
 9     if (!cacheValid) {
10       // 在獲取寫鎖以前必須釋放讀鎖,不釋放的話下面寫鎖會獲取不成功,形成死鎖
11       rwl.readLock().unlock();
12      // 獲取寫鎖
13       rwl.writeLock().lock();
14       try {
15         // 從新檢查state,由於在獲取寫鎖以前其餘線程可能已經獲取寫鎖而且更改了state
16         if (!cacheValid) {
17           data = ...
18           cacheValid = true;
19         }
20         // 經過在釋放寫鎖定以前獲取讀鎖定來降級
21         // 這裏再次獲取讀鎖,若是不獲取,那麼當寫鎖釋放後可能其餘寫線程再次得到寫鎖,致使下方`use(data)`時出現不一致的現象
22         // 這個操做就是降級
23         rwl.readLock().lock();
24       } finally {
25         rwl.writeLock().unlock(); // 釋放寫鎖,因爲在釋放以前讀鎖已經被獲取,因此如今是讀鎖獲取狀態
26       }
27     }
28 
29     try {
30     // 使用完後釋放讀鎖
31       use(data);
32     } finally {
33       rwl.readLock().unlock(); //釋放讀鎖
34     }
35   }
36  }}
相關文章
相關標籤/搜索