深入理解java:2.3.2. 並發編程concurrent包 之重入鎖/讀寫鎖/條件鎖


重入鎖
Java中的重入鎖(即ReentrantLock)   與JVM內置鎖(即synchronized)一樣,是一種排它鎖。

ReentrantLock提供了多樣化的同步,比如有時間限制的同步(定時鎖),可以被Interrupt的同步,即中斷鎖 (synchronized的同步是不能Interrupt的)等。

 

在資源競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,

但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態

Atomic原子操作,不激烈情況下,性能比synchronized略遜,而激烈的時候,也能維持常態。

激烈的時候,Atomic的性能會優於ReentrantLock一倍左右。

但是其有一個缺點,就是只能同步一個值,一段代碼中只能出現一個Atomic的變量,多於一個同步無效。因為他不能在多個Atomic之間同步。
所以,我們寫同步的時候,優先考慮synchronized,如果有特殊需要,再進一步優化。ReentrantLock和Atomic如果用的不好,不僅不能提高性能,還可能帶來災難。

 

synchronized是在JVM層面上實現的,在代碼執行時出現異常,JVM會自動釋放鎖定。

但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中

try{
  renentrantLock.lock();
  // 用戶操作
} finally {
  renentrantLock.unlock();
}

 

ReentrantLock獲取鎖定與三種方式:
    a)  lock(), 如果獲取了鎖立即返回,如果別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖

    b) tryLock(), 如果獲取了鎖立即返回true,如果別的線程正持有鎖,立即返回false;

    c) tryLock(long timeout,TimeUnit unit),   如果獲取了鎖定立即返回true,如果別的線程正持有鎖,會等待參數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false;

    d) lockInterruptibly:如果獲取了鎖定立即返回,如果沒有獲取鎖定,當前線程處於休眠狀態,直到獲得鎖定,或者當前線程被別的線程中斷

 

 

重入鎖可定義為公平鎖或非公平鎖,默認實現為非公平鎖

公平鎖是指多個線程獲取鎖被阻塞的情況下,鎖變為可用時,最先申請鎖的線程獲得鎖

在並發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空,或者當前線程線程是等待隊列的第一個,就占有鎖,否則就會加入到等待隊列中,以后會按照FIFO的規則從隊列中取到自己

可通過在重入鎖(RenentrantLock)的構造方法中傳入true構建公平鎖,如Lock lock = new RenentrantLock(true)


非公平鎖是指多個線程等待鎖的情況下,鎖變為可用狀態時,哪個線程獲得鎖是隨機的

synchonized相當於非公平鎖。可通過在重入鎖的構造方法中傳入false或者使用無參構造方法構建非公平鎖。

 

讀寫鎖
鎖可以保證原子性和可見性。

而原子性更多是針對寫操作而言。對於讀多寫少的場景,一個讀操作無須阻塞其它讀操作,只需要保證讀和寫  或者 寫與寫  不同時發生即可。

此時,如果使用重入鎖(即排它鎖),對性能影響較大。Java中的讀寫鎖(ReadWriteLock)就是為這種讀多寫少的場景而創造的。

實際上,ReadWriteLock接口並非繼承自Lock接口,ReentrantReadWriteLock也只實現了ReadWriteLock接口而未實現Lock接口。

ReadLock和WriteLock,是ReentrantReadWriteLock類的靜態內部類,它們實現了Lock接口。


一個ReentrantReadWriteLock實例包含一個ReentrantReadWriteLock.ReadLock實例和一個ReentrantReadWriteLock.WriteLock實例。

通過readLock()和writeLock()方法可分別獲得讀鎖實例和寫鎖實例,並通過Lock接口提供的獲取鎖方法獲得對應的鎖。


讀寫鎖的鎖定規則如下:
獲得讀鎖后,其它線程可獲得讀鎖而不能獲取寫鎖
獲得寫鎖后,其它線程既不能獲得讀鎖也不能獲得寫鎖

public class ReadWriteLockDemo {


  public static void main(String[] args) {
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();


    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 1 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 1 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();


    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 2 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();


    new Thread(() -> {
      Lock lock = readWriteLock.writeLock();
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 3 started with write lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 3 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}
執行結果如下
Sat Jun 18 21:33:46 CST 2016  Thread 1 started with read lock
Sat Jun 18 21:33:46 CST 2016  Thread 2 started with read lock
Sat Jun 18 21:33:48 CST 2016  Thread 2 ended
Sat Jun 18 21:33:48 CST 2016  Thread 1 ended
Sat Jun 18 21:33:48 CST 2016  Thread 3 started with write lock
Sat Jun 18 21:33:50 CST 2016  Thread 3 ended
從上面的執行結果可見,thread 1和thread 2都只需獲得讀鎖,因此它們可以並行執行。

而thread 3因為需要獲取寫鎖,必須等到thread 1和thread 2釋放鎖后才能獲得鎖。

 

條件鎖
條件鎖只是一個幫助用戶理解的概念,實際上並沒有條件鎖這種鎖。

對於每個重入鎖,都可以通過newCondition()方法綁定若干個條件對象。

 

重入鎖可以創建若干個條件對象,signal()和signalAll()方法只能喚醒相同條件對象的等待。
一個重入鎖上可以生成多個條件變量,不同線程可以等待不同的條件,從而實現更加細粒度的的線程間通信。

Condition是個接口,基本的方法就是await()和signal()方法;

Condition依賴於Lock接口,生成一個Condition的基本代碼是lock.newCondition()
調用Condition的await()和signal()方法,都必須在lock保護之內,就是說必須在lock.lock()和lock.unlock之間才可以使用

Conditon中的await()對應Object的wait();
Condition中的signal()對應Object的notify();
Condition中的signalAll()對應Object的notifyAll()。

 

原理

ReentrantLock實現的前提就是AbstractQueuedSynchronizer,抽象隊列同步器,簡稱AQS,是java.util.concurrent的核心。

ReentrantLock中有一個抽象類Sync繼承了AQS。

 

我們知道Lock的本質是AQS,AQS自己維護的隊列是當前等待資源的隊列,AQS會在被釋放后,依次喚醒隊列中從前到后的所有節點,使他們對應的線程恢復執行,直到隊列為空。

而Condition自己也維護了一個隊列,該隊列的作用是維護一個等待signal信號的隊列。

但是,兩個隊列的作用不同的,事實上,每個線程也僅僅會同時存在以上兩個隊列中的一個,流程是這樣的:

1. 線程1調用reentrantLock.lock時,嘗試獲取鎖。如果成功,則返回,從AQS的隊列中移除線程;否則阻塞,保持在AQS的等待隊列中。
2. 線程1調用await方法被調用時,對應操作是被加入到Condition的等待隊列中,等待signal信號;同時釋放鎖。
3. 鎖被釋放后,會喚醒AQS隊列中的頭結點,所以線程2會獲取到鎖。
4. 線程2調用signal方法,這個時候Condition的等待隊列中只有線程1一個節點,於是它被取出來,並被加入到AQS的等待隊列中。注意,這個時候,線程1 並沒有被喚醒,只是被加入AQS等待隊列。
5. signal方法執行完畢,線程2調用unLock()方法,釋放鎖。這個時候因為AQS中只有線程1,於是,線程1被喚醒,線程1恢復執行。


所以:
發送signal信號只是將Condition隊列中的線程加到AQS的等待隊列中。

只有到發送signal信號的線程調用reentrantLock.unlock()釋放鎖后,這些線程才會被喚醒。

可以看到,整個協作過程是靠結點在AQS的等待隊列和Condition的等待隊列中來回移動實現的,

Condition作為一個條件類,很好的自己維護了一個等待信號的隊列,並在適時的時候將結點加入到AQS的等待隊列中來實現的喚醒操作。

 

signal就是喚醒Condition隊列中的第一個非CANCELLED節點線程,

而signalAll就是喚醒所有非CANCELLED節點線程,本質是將節點從Condition隊列中取出來一個還是所有節點放到AQS的等待隊列。

盡管所有Node可能都被喚醒,但是要知道的是仍然只有一個線程能夠拿到鎖,其它沒有拿到鎖的線程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

 

阻塞當前的線程:調用JNI的 unsafe.park(false, 0L)

操作隊列時,調用JNI的unsafe.compareAndSwapInt(),循環CAS。

compareAndSetHead(Node update) 利用CAS設置頭Node
compareAndSetTail(Node expect, Node update) 利用CAS設置尾Node
compareAndSetWaitStatus(Node node, int expect, int update) 利用CAS設置某個Node中的等待狀態


免責聲明!

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



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