天天看點

ReentrantReadWriteLock/ReentrantLock 重入鎖

轉自:http://blog.csdn.net/vking_wang/article/details/9952063 (【Java線程】鎖機制:synchronized、Lock、Condition)

一.ReentrantReadWriteLock(讀寫鎖)的使用

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

讀寫鎖:分為讀鎖和寫鎖,多個讀鎖不互斥(共享讀鎖),讀鎖與寫鎖互斥(互斥寫鎖),這是由jvm自己控制的,你隻要上好相應的鎖即可。如果你的代碼隻讀資料,可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改資料,隻能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!

ReentrantReadWriteLock會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖

線程進入讀鎖的前提條件:

    沒有其他線程的寫鎖,

    沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個

線程進入寫鎖的前提條件:

    沒有其他線程的讀鎖

    沒有其他線程的寫鎖

提到ReentrantReadWriteLock,首先要做的是與ReentrantLock劃清界限。它和後者都是單獨的實作,彼此之間沒有繼承或實作的關系。

然後就是總結這個鎖機制的特性了: 

     (a).重入方面其内部的WriteLock可以擷取ReadLock,但是反過來ReadLock想要獲得WriteLock則永遠都不要想。 

     (b).WriteLock可以降級為ReadLock,順序是:先獲得WriteLock再獲得ReadLock,然後釋放WriteLock,這時候線程将保持Readlock的持有。反過來ReadLock想要更新為WriteLock則不可能,為什麼?參看(a)

     (c).ReadLock可以被多個線程持有并且在作用時排斥任何的WriteLock,而WriteLock則是完全的互斥。這一特性最為重要,因為對于高讀取頻率而相對較低寫入的資料結構,使用此類鎖同步機制則可以提高并發量。 

     (d).不管是ReadLock還是WriteLock都支援Interrupt,語義與ReentrantLock一緻。 

     (e).WriteLock支援Condition并且與ReentrantLock語義一緻,而ReadLock則不能使用Condition,否則抛出UnsupportedOperationException異常。  

---------- 以上摘抄自讀寫鎖的使用http://www.cnblogs.com/liuling/p/2013-8-21-03.html

ReentrantReadWriteLock 與 synchronized 的差別: 

1.拆分讀寫鎖場景. 提高并發量:

ReadLock可以被多個線程持有并且在作用時排斥任何的WriteLock,而WriteLock則是完全的互斥。這一特性最為重要,因為對于高讀取頻率而相對較低寫入的資料結構,使用此類鎖同步機制則可以提高并發量。 

2.不管是ReadLock還是WriteLock都支援Interrupt,語義與ReentrantLock一緻。 

3.寫鎖支援Condition重入.

使用ReentrantReadWriteLock 

1,放在成員變量的位置.

2,如果聲明為static 則表示是類鎖.對執行個體對象共享.

   如果沒有聲明為static 則表示是對象鎖. 對單列對象共享.對多個new出來的對象不共享.

3,使用時聲明為final. 表示不允許修改引用指向.

4,一把對象鎖在多個同步代碼中的使用如下.

ReentrantReadWriteLock/ReentrantLock 重入鎖

二. ReentrantLock(重入鎖)的使用

synchronized原語和ReentrantLock在一般情況下沒有什麼差別,但是在非常複雜的同步應用中,請考慮使用ReentrantLock,

特别是遇到下面幾種種需求的時候。 

1.某個線程在等待一個鎖的控制權的這段時間需要中斷 

2.需要分開處理一些wait-notify,ReentrantLock裡面的Condition應用,能夠控制notify哪個線程 

3.具有公平鎖功能,每個到來的線程都将排隊等候 

先說第一種情況

ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B2個線程去競争鎖,A線程得到了鎖,B線程等待,但是A線程這個時候實在有太多事情要處理,就是一直不傳回,B線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。

這個時候ReentrantLock就提供了2種機制,

第一,B線程中斷自己(或者别的線程中斷它),但是ReentrantLock不去響應,繼續讓B線程等待,你再怎麼中斷,我全當耳邊風(synchronized原語就是如此);

第二,B線程中斷自己(或者别的線程中斷它),ReentrantLock處理了這個中斷,并且不再等待這個鎖的到來,完全放棄。

ReentrantLock是一個互斥的同步器,其實作了接口Lock,裡面的功能函數主要有:

1. ‍lock() -- 阻塞模式擷取資源

2. ‍lockInterruptibly() -- 可中斷模式擷取資源

3. ‍tryLock() -- 嘗試擷取資源

4. tryLock(time) -- 在一段時間内嘗試擷取資源

5. ‍unlock() -- 釋放資源

ReentrantLock實作Lock有兩種模式即公平模式和不公平模式

Concurrent包下的同步器都是基于AQS架構,在ReentrantLock裡面會看到這樣三個類

-----------------------------------------------------------------------

static abstract class Sync extends AbstractQueuedSynchronizer {

    abstract void lock();

    final boolean nonfairTryAcquire(int acquires) { ... }

    protected final boolean tryRelease(int releases) { ... }

}

-----------------------------------------------------------------------

final static class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires) { ... }

    final void lock() { ... }

}

-----------------------------------------------------------------------

final static class FairSync extends Sync {

    final void lock() { ... }

    protected final boolean tryAcquire(int acquires) { ... }

}

-----------------------------------------------------------------------

再回歸到ReentrantLock對Lock的實作上

0. ‍ReentrantLock執行個體化

   ReentrantLock有個屬性sync,實際上對Lock接口的實作都是包裝了一下這個sync的實作

   如果是公平模式則建立一個FairSync對象,否則建立一個NonfairSync對象,預設是不公平模式

1. lock() 調用sync.lock()

   公平模式下:直接走AQS的acquire函數,此函數的邏輯走一次tryAcquire,如果成功

   線程拜托同步器的控制,否則加入NODE連結清單,進入acquireQueued的tryAcquire,休眠,被喚醒的輪回

   不公平模式下和公平模式下邏輯大體上是一樣的,不同點有兩個:

   a. 在執行tryAcquire之前的操作,不公平模式會直接compareAndSetState(0, 1)原子性的設定AQS的資源

   0表示目前沒有線程占據資源,則直接搶占資源,不管AQS的NODE連結清單的FIFO原則

   b. tryAcquire的原理不一樣,不公平模式的tryAcquire隻看compareAndSetState(0, 1)能否成功

   而公平模式還會加一個條件就是此線程對于的NODE是不是NODE連結清單的第一個

   c. 由于tryAcquire的實作不一樣,而公平模式和不公平模式在lock期間走的邏輯是一樣的(AQS的acquireQueued的邏輯)

   d. 對于一個線程在擷取到資源後再調用lock會導緻AQS的資源做累加操作,同理線程要徹底的釋放資源就必須同樣

   次數的調用unlock來做對應的累減操作,因為對應ReentrantLock來說tryAcquire成功一個必須的條件就是compareAndSetState(0, 1)

   e. 由于acquireQueued過程中屏蔽了線程中斷,隻是線上程拜托同步器控制後,如果記錄線程在此期間被中斷過則标記線程的

   中斷狀态

2. ‍lockInterruptibly() 調用sync.acquireInterruptibly(1),上一篇文章講過AQS的核心函數,這個過程和acquireQueued

   是一樣的,隻不過在阻塞期間如果被标記中斷則線程在park期間被喚醒,然後直接退出那個輪回,抛出中斷異常

   由于公平模式和不公平模式下對tryAcquire的實作不一樣導緻‍lockInterruptibly邏輯也是不一樣

3. tryLock() 函數隻是嘗試性的去擷取一下鎖,跟tryAcquire一樣,這兩種模式下走的代碼一樣都是公平模式下的代碼

4. tryLock(time) 調用sync.tryAcquireNanos(time),上一篇文章講過AQS的核心函數,這個過程和acquireQueued一樣,

   a. 在阻塞前會先計算阻塞的時間,進入休眠

   b. 如果被中斷則會判斷時間是否到了

      1. 如果沒到則且被其他線程設定了中斷标志,退出那個輪回,抛出中斷異常,如果沒有被設定中斷标記則是前一個線程

      釋放了資源再喚醒了它,其繼續走那個輪回,輪回中,如果tryAcquire成功則擺脫了同步器的控制,否則回到a

      2. 如果時間到了則退出輪回,擷取資源失敗

5. ‍unlock() 調用sync.release(1),上一篇文章講過AQS的核心函數,release函數會調用Sync實作的tryRelease函數來判斷

   釋放資源是否成功,即Sync.tryRelease函數,其邏輯過程是

   a. 首先判斷目前占據資源的線程是不是調用者,如果不是會抛出異常IllegalMonitorStateException

   b. 如果是則進行AQS資源的減1邏輯,如果再減1後AQS資源變成0則表示調用線程測得放棄了此鎖,傳回給release的值的TRUE,

   release會喚醒下一個線程

-----------------------------------------------------------------------

整體來看ReentrantLock互斥鎖的實作大緻是

1. 自己實作AQS的tryAcquire和tryRelease邏輯,tryAcquire表示嘗試去擷取鎖,tryRelease表示嘗試去釋放鎖

2. ReentrantLock對lock(),trylock(),trylock(time),unlock()的實作都是使用AQS的架構,然後AQS的架構又傳回調用

ReentrantLock實作的tryAcquire和tryRelease來對線程是否擷取鎖和釋放鎖成功做出依據判斷

---------以上摘抄自重入鎖的使用 http://blog.csdn.net/eclipser1987/article/details/7301828

第二種情況: 線程間通信Condition

Condition可以替代傳統的線程間通信,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——為什麼方法名不直接叫wait()/notify()/nofityAll()?因為Object的這幾個方法是final的,不可重寫!

傳統線程的通信方式,Condition都可以實作。

注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。

Condition的強大之處在于它可以為多個線程間建立不同的Condition

看JDK文檔中的一個例子:

假定有一個綁定的緩沖區,它支援 put 和 take 方法。如果試圖在空的緩沖區上執行 take 操作,則在某一個項變得可用之前,線程将一直阻塞;如果試圖在滿的緩沖區上執行 put 操作,則在有空間變得可用之前,線程将一直阻塞。

我們喜歡在單獨的等待 set 中儲存put 線程和take 線程,這樣就可以在緩沖區中的項或空間變得可用時利用最佳規劃,一次隻通知一個線程。

可以使用兩個Condition 執行個體來做到這一點。

——其實就是java.util.concurrent.ArrayBlockingQueue的功能

  1. class BoundedBuffer {  
  1.   final Lock lock = new ReentrantLock();          //鎖對象  
  2.   final Condition notFull  = lock.newCondition(); //寫線程鎖  
  3.   final Condition notEmpty = lock.newCondition(); //讀線程鎖  
  4.   final Object[] items = new Object[100];//緩存隊列  
  5.   int putptr;  //寫索引  
  6.   int takeptr; //讀索引  
  7.   int count;   //隊列中資料數目  
  8.   //寫  
  9.   public void put(Object x) throws InterruptedException {  
  10.     lock.lock(); //鎖定  
  11.     try {  
  12.       // 如果隊列滿,則阻塞<寫線程>  
  13.       while (count == items.length) {  
  14.         notFull.await();   
  15.       }  
  16.       // 寫入隊列,并更新寫索引  
  17.       items[putptr] = x;   
  18.       if (++putptr == items.length) putptr = 0;   
  19.       ++count;  
  20.       // 喚醒<讀線程>  
  21.       notEmpty.signal();   
  22.     } finally {   
  23.       lock.unlock();//解除鎖定   
  24.     }   
  25.   }  
  26.   //讀   
  27.   public Object take() throws InterruptedException {   
  28.     lock.lock(); //鎖定   
  29.     try {  
  30.       // 如果隊列空,則阻塞<讀線程>  
  31.       while (count == 0) {  
  32.          notEmpty.await();  
  33.       }  
  34.       //讀取隊列,并更新讀索引  
  35.       Object x = items[takeptr];   
  36.       if (++takeptr == items.length) takeptr = 0;  
  37.       --count;  
  38.       // 喚醒<寫線程>  
  39.       notFull.signal();   
  40.       return x;   
  41.     } finally {   
  42.       lock.unlock();//解除鎖定   
  43.     }   
  44.   }   

優點:

假設緩存隊列中已經存滿,那麼阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。

那麼假設隻有一個Condition會有什麼效果呢?緩存隊列中已經存滿,這個Lock不知道喚醒的是讀線程還是寫線程了,如果喚醒的是讀線程,皆大歡喜,如果喚醒的是寫線程,那麼線程剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了很多時間。

----- 以上摘抄自線程間通信 http://blog.csdn.net/vking_wang/article/details/9952063