Java中的讀寫鎖


一、讀寫鎖

1、初識讀寫鎖

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

  b)關於讀寫鎖的基本使用:在不使用讀寫鎖的時候,一般情況下我們需要使用synchronized搭配等待通知機制完成並發控制(寫操作開始的時候,所有晚於寫操作的讀操作都會進入等待狀態),只有寫操作完成並通知后才會將等待的線程喚醒繼續執行。

  如果改用讀寫鎖實現,只需要在讀操作的時候獲取讀鎖,寫操作的時候獲取寫鎖。當寫鎖被獲取到的時候,后續操作(讀寫)都會被阻塞,只有在寫鎖釋放之后才會執行后續操作。並發包中對ReadWriteLock接口的實現類是ReentrantReadWriteLock,這個實現類具有下面三個特點

  ①具有與ReentrantLock類似的公平鎖和非公平鎖的實現:默認的支持非公平鎖,對於二者而言,非公平鎖的吞吐量由於公平鎖;

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

  ③鎖能降級:遵循獲取寫鎖、獲取讀鎖在釋放寫鎖的順序,即寫鎖能夠降級為讀鎖

2、讀寫鎖源碼分析

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

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

 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     }

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

  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  }}

 


免責聲明!

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



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