鎖——Lock、Condition、ReadWriteLock、LockSupport

weixin_33816300發表於2018-07-19

該系列依舊是參照android api27原始碼來學習。

關於鎖的種類概念,可以參考 Java中的鎖分類

Lock介面

該介面位於java.util.concurrent.locks包下。

相對synchronized而言,Lock介面提供了更多可擴充套件的鎖操作,有更靈活的結構,完全不同的屬性,以及支援多個相關聯的Condition物件。

鎖是控制多執行緒訪問共享變數的工具。通常鎖提供對共享變數的獨佔訪問,即同一時間只有一個執行緒可以獲得鎖,並且想要訪問共享變數就要先得到鎖。然而,有些鎖允許併發訪問共享變數,比如ReadWriteLock中的讀鎖。

synchronized語法是塊結構,可以自動完成鎖的釋放;如果synchronized有巢狀使用,則鎖的釋放順序和獲取順序是相反的。
而Lock必須手動控制鎖的釋放,但可以靈活控制釋放順序。語法結構如下:

 Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   //確保鎖一定會被釋放
   l.unlock();
 }

Lock相對synchronized還提供了以下功能:

  1. 提供了非阻塞嘗試獲取鎖的方法tryLock()
  2. 提供了嘗試獲取可被中斷的鎖的方法lockInterruptibly()
  3. 提供了獲取具有時效的鎖的方法tryLock(long, TimeUnit)

Lock還可以提供其他的行為,比如保證順序、非重入、死鎖檢測等。

Lock物件也可以作為synchronized語法中被鎖住的物件,這和呼叫Lock物件自身的lock()方法沒有任何關係。不過為了避免混淆,不推薦這樣使用,除非是在Lock實現類的內部。

注:除非特別說明,否則傳入null作為任何的引數將導致NullPointerException。

該介面的方法如下:

void lock()

阻塞方法,獲得一個鎖。如果鎖暫時不可用,那麼當前執行緒會休眠(被阻塞,進入WAITING狀態),直到獲取鎖。

void lockInterruptibly() throws InterruptedException

阻塞方法,獲取鎖,除非當前執行緒被中斷。若當前鎖不可用,則執行緒阻塞,直到以下任意條件成立:

  • 當前執行緒獲得鎖。
  • 當前執行緒被中斷,並且支援鎖的獲取過程被中斷。

如果當前執行緒滿足以下任意條件,則丟擲InterruptedException,並且清除當前執行緒的中斷狀態:

  • 在呼叫該方法之前,已經處於中斷狀態。
  • 在獲取鎖的時候被中斷,並且支援在獲取鎖的時候被中斷。

boolean tryLock()

非阻塞方法。若鎖可用,則獲取鎖,返回true;否則返回false。通常用法如下:

 Lock lock = ...;
 if (lock.tryLock()) {
   try {
     // manipulate protected state
   } finally {
     lock.unlock();
   }
 } else {
   // perform alternative actions
 }

boolean tryLock(long time, TimeUnit unit) throws InterruptedException

如果鎖在等待時間內可用,並且當前執行緒未被中斷,則獲取鎖。
鎖可用,則立即返回true。否則,執行緒阻塞,知道以下任意條件觸發:

  • 當前執行緒獲得鎖.
  • 當前執行緒被中斷,並且支援鎖的獲取過程被中斷。
  • 等待超時。

若當前執行緒滿足如下任意條件,則丟擲InterruptedException,並且清除執行緒的中斷狀態:

  • 在呼叫該方法之前,已經處於中斷狀態。
  • 在獲取鎖的時候被中斷,並且支援在獲取鎖的時候被中斷。

若等待時間不大於0,則不會等待。

void unlock()

釋放鎖。

Condition newCondition()

返回一個繫結到該Lock例項的Condition例項。
當前執行緒在呼叫Condition的等待方法之前,需要先獲取鎖。呼叫Condition的await()方法會釋放鎖,並在等待結束時重新獲取鎖。


Condition介面

Condition從Lock中獲取,且每個Lock可以有多個Condition,彼此互不影響。await、signal方法等同於Object的wait、notify方法。Lock可以代替synchronizedde的使用,Condition可以代替Object的wait/notify方法的使用。

Condition需要與Lock繫結使用,通過Lock.newCondition方法獲取。

舉個例子。假設有一個支援put和take阻塞方法的有界buffer,所實現的生產者/消費者模型如下:

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }

ArrayBlockingQueue類正是這樣的實現。

void await() throws InterruptedException

阻塞當前執行緒,直到被喚醒或者中斷。與Condition相關聯的Lock會被釋放。在該方法返回之前,當前執行緒必須重新獲取與Condition關聯的鎖。

當前執行緒滿足以下任意條件,則丟擲InterruptedException,並且清除中斷狀態:

  1. 呼叫await之前,已處於中斷狀態。
  2. 阻塞過程中被中斷,並且支援執行緒掛起時被中斷。

void awaitUninterruptibly()

與await()方法相比,不受中斷影響,也不影響中斷狀態,只能被喚醒。

long awaitNanos(long nanosTimeout) throws InterruptedException

與await()相比,新增了超時時間。該方法返回剩餘等待時間,若超時,則返回值不大於0。通常用法如下:

 boolean aMethod(long timeout, TimeUnit unit) {
   long nanos = unit.toNanos(timeout);
   lock.lock();
   try {
     while (!conditionBeingWaitedFor()) {
       if (nanos <= 0L)
         return false;
       nanos = theCondition.awaitNanos(nanos);
     }
     // ...
   } finally {
     lock.unlock();
   }
 }

boolean await(long time, TimeUnit unit) throws InterruptedException

該方法在效果上等同於awaitNanos(unit.toNanos(time)) > 0

boolean awaitUntil(Date deadline) throws InterruptedException

與await()相比,新增了等待截止時間,返回值表示是否超過了截止時間。通常用法如下:

 boolean aMethod(Date deadline) {
   boolean stillWaiting = true;
   lock.lock();
   try {
     while (!conditionBeingWaitedFor()) {
       if (!stillWaiting)
         return false;
       stillWaiting = theCondition.awaitUntil(deadline);
     }
     // ...
   } finally {
     lock.unlock();
   }
 }

void signal()

喚醒一個等待執行緒,效果與Object.notify()類似。

void signalAll()

喚醒所有等待執行緒,效果與Object.notifyAll()類似。


ReadWriteLock

ReadWriteLock維護一對相關聯的Lock,一個針對讀操作,一個針對寫操作。讀鎖可以被多個執行緒同時持有,只要沒有其他執行緒寫入。而寫鎖是獨享的。

ReadWriteLock的實現類必須保證讀寫操作的記憶體同步,也即成功獲得讀鎖後,將可以看到上次釋放寫鎖後的所有更新。

讀寫鎖比互斥鎖有更大的吞吐量。在只有一個執行緒寫資料的時候,大多數情況下可以有任意數量的執行緒併發讀取資料,讀寫鎖正是利用了這一點。當然這是針對多核處理器而言。

讀寫鎖相對於互斥鎖是否提升效能,依賴於對資料讀和寫的頻率、持續時間,以及資料爭用情況,即同一時間訪問資料的執行緒數。

雖然讀寫鎖的操作簡單直接,但實現類必須做出一些權衡,這會影響到效能:

  1. 在讀操作和寫操作都在等待的時候,恰逢一個寫操作釋放了寫鎖,這時是否授予讀鎖或者寫鎖?寫操作擁有高優先順序,因為讀寫鎖本身適用於讀多寫少的情況,所以寫操作佔用時間少,頻率低;讀操作擁有低優先順序,因為讀可能需要很久,導致寫操作進入漫長的等待;當然,公平起見,也可以按照請求的順序來實現。
  2. 在一個讀操作正在執行和一個寫操作正在等待的情況下,是否授予讀鎖給其他的讀操作。讀操作的效能可能無限期延後寫操作,而寫操作會降低併發執行的能力。
  3. 鎖是否可以重入?一個擁有寫鎖的執行緒可以再次請求寫鎖嗎?或者可以同時獲取讀寫鎖嗎?讀鎖可重入嗎?
  4. 在不允許介於中間的寫操作時,寫鎖可以降級成讀鎖嗎?擁有讀鎖的讀操作,可以優先於其他正在等待的讀或寫操作,將讀鎖升級為寫鎖嗎?

Lock readLock()

獲取讀鎖。

Lock writeLock()

獲取寫鎖。


LockSupport

不多介紹了,參考java併發包系列---LockSupport

相關文章