java 多線程之ReentrantLock與condition

來源:互聯網
上載者:User

標籤:null   stat   man   .net   blog   finally   href   word   unpark   

參考連結:8288251

ReentrantLock 類 1.1 什麼是reentrantlock java.util.concurrent.lock 中的 Lock 架構是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為 Lock 的多種實現留下了空間,各種實現可能有不同的調度演算法、效能特性或者鎖定語義。 ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的並發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的效能。(換句話說,當許多線程都想訪問共用資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。) reentrant 鎖意味著什麼呢?簡單來說,它有一個與鎖相關的擷取計數器,如果擁有鎖的某個線程再次得到鎖,那麼擷取計數器就加1,然後鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果線程進入由線程已經擁有的監控器保護的 synchronized 塊,就允許線程繼續進行,當線程退出第二個(或者後續) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。 1.2 ReentrantLock與synchronized的比較  

相同:ReentrantLock提供了synchronized類似的功能和記憶體語義。

不同:

(1)ReentrantLock功能性方面更全面,比如時間鎖等候,可中斷鎖等候,鎖投票等,因此更有擴充性。在多個條件變數和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了Condition,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition執行個體,所以更有擴充性。

(2)ReentrantLock 的效能比synchronized會好點。

(3)ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續處理,取得不成功,可以等下次啟動並執行時候處理,所以不容易產生死結,而synchronized則一旦進入鎖請求要麼成功,要麼一直阻塞,所以更容易產生死結。

 

1.3 ReentrantLock擴充的功能 

 

1.3.1 實現可輪詢的鎖請求  在內部鎖中,死結是致命的——唯一的恢複方法是重新啟動程式,唯一的預防方法是在構建程式時不要出錯。而可輪詢的鎖擷取模式具有更完善的錯誤恢複機制,可以規避死結的發生。 
如果你不能獲得所有需要的鎖,那麼使用可輪詢的擷取方式使你能夠重新拿到控制權,它會釋放你已經獲得的這些鎖,然後再重新嘗試。可輪詢的鎖擷取模式,由tryLock()方法實現。此方法僅在調用時鎖為空白閑狀態才擷取該鎖。如果鎖可用,則擷取鎖,並立即傳回值true。如果鎖不可用,則此方法將立即傳回值false。此方法的典型使用語句如下: 
  1.  Lock lock = ...;
  2.  if (lock.tryLock()) {
  3.  try {
  4.  // manipulate protected state
  5.  } finally {
  6.  lock.unlock();
  7.  }
  8.  } else {
  9.  // perform alternative actions
  10.  }

1.3.2 實現可定時的鎖請求  當使用內部鎖時,一旦開始請求,鎖就不能停止了,所以內部鎖給實現具有時限的活動帶來了風險。為瞭解決這一問題,可以使用定時鎖。當具有時限的活 
動調用了阻塞方法,定時鎖能夠在時間預算內設定相應的逾時。如果活動在期待的時間內沒能獲得結果,定時鎖能使程式提前返回。可定時的鎖擷取模式,由tryLock(long, TimeUnit)方法實現。 
1.3.3 實現可中斷的鎖擷取請求  可中斷的鎖擷取操作允許在可取消的活動中使用。lockInterruptibly()方法能夠使你獲得鎖的時候響應中斷。

 

 1.4 ReentrantLock不好與需要注意的地方 (1) lock 必須在 finally 塊中釋放。否則,如果受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什麼,但是實際上,它極為重要。忘記在 finally 塊中釋放鎖,可能會在程式中留下一個定時炸彈,當有一天炸彈爆炸時,您要花費很大力氣才有找到源頭在哪。而使用同步,JVM 將確保鎖會獲得自動釋放(2) 當 JVM 用 synchronized 管理鎖定請求和釋放時,JVM 在產生線程轉儲時能夠包括鎖定資訊。這些對調試非常有價值,因為它們能標識死結或者其他異常行為的來源。 Lock 類只是普通的類,JVM 不知道具體哪個線程擁有 Lock 對象。 二、條件變數Condition 

條件變數很大一個程度上是為瞭解決Object.wait/notify/notifyAll難以使用的問題。

條件(也稱為條件隊列 或條件變數)為線程提供了一個含義,以便在某個狀態條件現在可能為 true 的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因為訪問此共用狀態資訊發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式 釋放相關的鎖,並掛起當前線程,就像 Object.wait 做的那樣。

上述API說明表明條件變數需要與鎖綁定,而且多個Condition需要綁定到同一鎖上。前面的Lock中提到,擷取一個條件變數的方法是Lock.newCondition()。

 

  1.  void await() throws InterruptedException;
  2.   
  3.  void awaitUninterruptibly();
  4.   
  5.  long awaitNanos(long nanosTimeout) throws InterruptedException;
  6.   
  7.  boolean await(long time, TimeUnit unit) throws InterruptedException;
  8.   
  9.  boolean awaitUntil(Date deadline) throws InterruptedException;
  10.   
  11.  void signal();
  12.   
  13.  void signalAll();


 

以上是Condition介面定義的方法,await*對應於Object.waitsignal對應於Object.notifysignalAll對應於Object.notifyAll。特別說明的是Condition的介面改變名稱就是為了避免與Object中的wait/notify/notifyAll的語義和使用上混淆,因為Condition同樣有wait/notify/notifyAll方法。

每一個Lock可以有任意資料的Condition對象,Condition是與Lock綁定的,所以就有Lock的公平性特性:如果是公平鎖,線程為按照FIFO的順序從Condition.await中釋放,如果是非公平鎖,那麼後續的鎖競爭就不保證FIFO順序了。

一個使用Condition實現生產者消費者的模型例子如下。

 

  1.  import java.util.concurrent.locks.Condition;
  2.  import java.util.concurrent.locks.Lock;
  3.  import java.util.concurrent.locks.ReentrantLock;
  4.   
  5.  public class ProductQueue<T> {
  6.   
  7.   private final T[] items;
  8.   
  9.   private final Lock lock = new ReentrantLock();
  10.   
  11.   private Condition notFull = lock.newCondition();
  12.   
  13.   private Condition notEmpty = lock.newCondition();
  14.   
  15.   //
  16.   private int head, tail, count;
  17.   
  18.   public ProductQueue(int maxSize) {
  19.   items = (T[]) new Object[maxSize];
  20.   }
  21.   
  22.   public ProductQueue() {
  23.   this(10);
  24.   }
  25.   
  26.   public void put(T t) throws InterruptedException {
  27.   lock.lock();
  28.   try {
  29.   while (count == getCapacity()) {
  30.   notFull.await();
  31.   }
  32.   items[tail] = t;
  33.   if (++tail == getCapacity()) {
  34.   tail = 0;
  35.   }
  36.   ++count;
  37.   notEmpty.signalAll();
  38.   } finally {
  39.   lock.unlock();
  40.   }
  41.   }
  42.   
  43.   public T take() throws InterruptedException {
  44.   lock.lock();
  45.   try {
  46.   while (count == 0) {
  47.   notEmpty.await();
  48.   }
  49.   T ret = items[head];
  50.   items[head] = null;//GC
  51.   //
  52.   if (++head == getCapacity()) {
  53.   head = 0;
  54.   }
  55.   --count;
  56.   notFull.signalAll();
  57.   return ret;
  58.   } finally {
  59.   lock.unlock();
  60.   }
  61.   }
  62.   
  63.   public int getCapacity() {
  64.   return items.length;
  65.   }
  66.   
  67.   public int size() {
  68.   lock.lock();
  69.   try {
  70.   return count;
  71.   } finally {
  72.   lock.unlock();
  73.   }
  74.   }
  75.   
  76.  }


 

在這個例子中消費take()需要 隊列不為空白,如果為空白就掛起(await()),直到收到notEmpty的訊號;生產put()需要隊列不滿,如果滿了就掛起(await()),直到收到notFull的訊號。

可能有人會問題,如果一個線程lock()對象後被掛起還沒有unlock,那麼另外一個線程就拿不到鎖了(lock()操作會掛起),那麼就無法通知(notify)前一個線程,這樣豈不是“死結”了?

 

2.1 await* 操作 

上一節中說過多次ReentrantLock是獨佔鎖,一個線程拿到鎖後如果不釋放,那麼另外一個線程肯定是拿不到鎖,所以在lock.lock()lock.unlock()之間可能有一次釋放鎖的操作(同樣也必然還有一次擷取鎖的操作)。我們再回頭看代碼,不管take()還是put(),在進入lock.lock()後唯一可能釋放鎖的操作就是await()了。也就是說await()操作實際上就是釋放鎖,然後掛起線程,一旦條件滿足就被喚醒,再次擷取鎖!

 

  1.  public final void await() throws InterruptedException {
  2.   if (Thread.interrupted())
  3.   throw new InterruptedException();
  4.   Node node = addConditionWaiter();
  5.   int savedState = fullyRelease(node);
  6.   int interruptMode = 0;
  7.   while (!isOnSyncQueue(node)) {
  8.   LockSupport.park(this);
  9.   if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  10.   break;
  11.   }
  12.   if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  13.   interruptMode = REINTERRUPT;
  14.   if (node.nextWaiter != null)
  15.   unlinkCancelledWaiters();
  16.   if (interruptMode != 0)
  17.   reportInterruptAfterWait(interruptMode);
  18.  }


 

上面是await()的程式碼片段。上一節中說過,AQS在擷取鎖的時候需要有一個CHL的FIFO隊列,所以對於一個Condition.await()而言,如果釋放了鎖,要想再一次擷取鎖那麼就需要進入隊列,等待被通知擷取鎖。完整的await()操作是安裝如下步驟進行的:

    1. 將當前線程加入Condition鎖隊列。特別說明的是,這裡不同於AQS的隊列,這裡進入的是Condition的FIFO隊列。後面會具體談到此結構。進行2。
    2. 釋放鎖。這裡可以看到將鎖釋放了,否則別的線程就無法拿到鎖而發生死結。進行3。
    3. 自旋(while)掛起,直到被喚醒或者逾時或者CACELLED等。進行4。
    4. 擷取鎖(acquireQueued)。並將自己從Condition的FIFO隊列中釋放,表明自己不再需要鎖(我已經拿到鎖了)。

這裡再回頭介紹Condition的資料結構。我們知道一個Condition可以在多個地方被await*(),那麼就需要一個FIFO的結構將這些Condition串聯起來,然後根據需要喚醒一個或者多個(通常是所有)。所以在Condition內部就需要一個FIFO的隊列。

 

  1.  private transient Node firstWaiter;
  2.  private transient Node lastWaiter;

 

上面的兩個節點就是描述一個FIFO的隊列。我們再結合前面提到的節點(Node)資料結構。我們就發現Node.nextWaiter就派上用場了!nextWaiter就是將一系列的Condition.await*串聯起來組成一個FIFO的隊列。

 

2.2 signal/signalAll 操作 

await*()清楚了,現在再來看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要將Condition.await*()中FIFO隊列中第一個Node喚醒(或者全部Node)喚醒。儘管所有Node可能都被喚醒,但是要知道的是仍然只有一個線程能夠拿到鎖,其它沒有拿到鎖的線程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

 

  1.  private void doSignal(Node first) {
  2.   do {
  3.   if ( (firstWaiter = first.nextWaiter) == null)
  4.   lastWaiter = null;
  5.   first.nextWaiter = null;
  6.   } while (!transferForSignal(first) &&
  7.   (first = firstWaiter) != null);
  8.  }
  9.   
  10.  private void doSignalAll(Node first) {
  11.   lastWaiter = firstWaiter = null;
  12.   do {
  13.   Node next = first.nextWaiter;
  14.   first.nextWaiter = null;
  15.   transferForSignal(first);
  16.   first = next;
  17.   } while (first != null);
  18.  }


 

上面的代碼很容易看出來,signal就是喚醒Condition隊列中的第一個非CANCELLED節點線程,而signalAll就是喚醒所有非CANCELLED節點線程。當然了遇到CANCELLED線程就需要將其從FIFO隊列中剔除。

 

  1.  final boolean transferForSignal(Node node) {
  2.   if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
  3.   return false;
  4.   
  5.   Node p = enq(node);
  6.   int c = p.waitStatus;
  7.   if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))
  8.   LockSupport.unpark(node.thread);
  9.   return true;
  10.  }


 

上面就是喚醒一個await*()線程的過程,根據前面的小節介紹的,如果要unpark線程,並使線程拿到鎖,那麼就需要線程節點進入AQS的隊列。所以可以看到在LockSupport.unpark之前調用了enq(node)操作,將當前節點加入到AQS隊列。

 

java 多線程之ReentrantLock與condition

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.