阻塞佇列 BlockingQueue(二)--ArrayBlockingQueue與LinkedBlockingQueue

eluanshi12發表於2018-12-14

原文:阻塞佇列 BlockingQueue

ArrayBlockingQueue

主要引數:

/** 儲存資料的陣列 */
final Object[] items;

/**獲取資料的索引,主要用於take,poll,peek,remove方法 */
int takeIndex;

/**新增資料的索引,主要用於 put, offer, or add 方法*/
int putIndex;

/** 佇列元素的個數 */
int count;

/** 控制並非訪問的鎖 */
final ReentrantLock lock;

/**notEmpty條件物件,用於通知take方法佇列已有元素,可執行獲取操作 */
private final Condition notEmpty;

/**notFull條件物件,用於通知put方法佇列未滿,可執行新增操作 */
private final Condition notFull;

/** 迭代器 */
transient Itrs itrs = null;

public ArrayBlockingQueue(int capacity) { 
  this(capacity, false);//預設構造非公平鎖的阻塞佇列 
} 
public ArrayBlockingQueue(int capacity, boolean fair) { 
  if (capacity <= 0)  
    throw new IllegalArgumentException(); 
  this.items = new Object[capacity]; 
  lock = new ReentrantLock(fair);//初始化ReentrantLock重入鎖,出隊入隊擁有這同一個鎖 
  notEmpty = lock.newCondition;//初始化非空等待佇列
  notFull = lock.newCondition;//初始化非滿等待佇列 
} 
public ArrayBlockingQueue(int capacity, boolean fair, Collecation<? extends E> c) { 
  this(capacity, fair); 
  final ReentrantLock lock = this.lock; 

    // 這個鎖的操作並不是為了互斥操作,而是保證其可見性。
    // 假如執行緒1是例項化ArrayBlockingQueue物件,執行緒2是對例項化的ArrayBlockingQueue物件做入隊操作
    // (當然要保證執行緒1和執行緒2的執行順序),如果不對它進行加鎖操作(加鎖會保證其可見性,也就是寫回主存)
    // 執行緒1的集合有可能只存線上程1維護的快取中,並沒有寫回主存
    // 執行緒2中例項化的ArrayBlockingQueue維護的快取以及主存中並沒有集合存在
    // 此時就因為可見性造成資料不一致的情況,引發執行緒安全問題。 
  lock.lock(); // Lock only for visibility, not mutual exclusion

  try { 
    int i = 0; 
    try { 
      for (E e : c) { 
        checkNotNull(e); 
        item[i++] = e;//將集合新增進陣列構成的佇列中 
      } 
    } catch (ArrayIndexOutOfBoundsException ex) { 
      throw new IllegalArgumentException(); 
    } 
    count = i;//佇列中的實際資料數量 
    putIndex = (i == capacity) ? 0 : i; 
  } finally { 
    lock.unlock(); 
  } 
}

ArrayBlockingQueue內部通過陣列物件items來儲存所有的資料並通過一個ReentrantLock來同時控制新增執行緒與移除執行緒的併發訪問。

notEmpty條件物件用於存放等待或喚醒呼叫take方法的執行緒,告訴他們佇列已有元素,可以執行獲取操作。

notFull條件物件是用於等待或喚醒呼叫put方法的執行緒,告訴它們,佇列未滿,可以執行新增元素的操作。

takeIndex代表的是下一個方法(take,poll,peek,remove)被呼叫時獲取陣列元素的索引。

putIndex則代表下一個方法(put, offer, or add)被呼叫時元素新增到陣列中的索引。

新增元素時阻塞

public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

public boolean offer(E e) {
     checkNotNull(e);//檢查元素是否為null
     final ReentrantLock lock = this.lock;
     lock.lock();//加鎖
     try {
         if (count == items.length)//判斷佇列是否滿
             return false;
         else {
             enqueue(e);//新增元素到佇列
             return true;
         }
     } finally {
         lock.unlock(); // 釋放鎖
     }
 }

//入隊操作
private void enqueue(E x) {
    //獲取當前陣列
    final Object[] items = this.items;
    //通過putIndex索引對陣列進行賦值
    items[putIndex] = x;
    //索引自增,如果已是最後一個位置,重新設定 putIndex = 0;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;//佇列中元素數量加1
    //喚醒呼叫take()方法的執行緒,執行元素獲取操作。
    notEmpty.signal();
}

enqueue方法內部通過putIndex索引直接將元素新增到陣列items中。

這裡需要注意的是當putIndex索引大小等於陣列長度時,需要將putIndex重新設定為0。這是因為當前佇列執行元素獲取時總是從佇列頭部獲取,而新增元素是從佇列尾部新增。所以當佇列索引(從0開始)與陣列長度相等時,需要從陣列頭部開始新增。

//put方法,阻塞時可中斷
public void put(E e) throws InterruptedException {
   checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();//該方法可中斷
    try {
        //當佇列元素個數與陣列長度相等時,無法新增元素
        while (count == items.length)
            //將當前呼叫執行緒掛起,新增到notFull條件佇列中等待喚醒
            notFull.await();
        enqueue(e);//如果佇列沒有滿直接新增。。
    } finally {
        lock.unlock();
    }
}

put方法是一個阻塞的方法,如果佇列元素已滿,那麼當前執行緒將會被notFull條件物件掛起加到等待佇列中,直到佇列有空檔才會喚醒執行新增操作。

但如果佇列沒有滿,那麼就直接呼叫enqueue(e)方法將元素加入到陣列佇列中。

移除元素時阻塞

移除元素有這些方法:poll、remove、take

poll方法獲取並移除此佇列的頭元素,若佇列為空,則返回 null

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
       //判斷佇列是否為null,不為null執行dequeue()方法,否則返回null
       return (count == 0) ? null : dequeue();
    } finally {
       lock.unlock();
    }
}
//刪除佇列頭元素並返回
private E dequeue() {
    //拿到當前陣列的資料
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    //獲取要刪除的物件
    E x = (E) items[takeIndex];
    將陣列中takeIndex索引位置設定為null
    items[takeIndex] = null;
    //takeIndex索引加1並判斷是否與陣列長度相等,
    //如果相等說明已到盡頭,恢復為0
    if (++takeIndex == items.length)
      takeIndex = 0;
    count--;//佇列個數減1
    if (itrs != null)
      itrs.elementDequeued();//同時更新迭代器中的元素資料
    //刪除了元素說明佇列有空位,喚醒notFull條件物件新增執行緒,執行新增操作
    notFull.signal();
    return x;
}

remove(Object o)方法的刪除過程相對複雜些,因為該方法並不是直接從佇列頭部刪除元素。

首先執行緒先獲取鎖,接著判斷佇列count>0,這點是保證併發情況下刪除操作安全執行。

接下來獲取下一個要新增源的索引putIndex以及takeIndex索引 ,作為後續迴圈的結束判斷,因為只要putIndex與takeIndex不相等就說明佇列沒有結束。

然後通過while迴圈找到要刪除的元素索引,執行removeAt(i)方法刪除。

在removeAt(i)方法中實際上做了兩件事:

  • 判斷佇列頭部元素是否為刪除元素,如果是直接刪除,並喚醒新增執行緒。
  • 如果要刪除的元素並不是佇列頭元素,那麼執行迴圈操作,從要刪除元素的索引removeIndex之後的元素都往前移動一個位置,那麼要刪除的元素就被removeIndex之後的元素替換,從而也就完成了刪除操作。
public boolean remove(Object o) {
    if (o == null) return false;
    //獲取陣列資料
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lock();//加鎖
    try {
        //如果此時佇列不為null,這裡是為了防止併發情況
        if (count > 0) {
            //獲取下一個要新增元素時的索引
            final int putIndex = this.putIndex;
            //獲取當前要被刪除元素的索引
            int i = takeIndex;
            //執行迴圈查詢要刪除的元素
            do {
                //找到要刪除的元素
                if (o.equals(items[i])) {
                    removeAt(i);//執行刪除
                    return true;//刪除成功返回true
                }
                //當前刪除索引執行加1後判斷是否與陣列長度相等
                //若為true,說明索引已到陣列盡頭,將i設定為0
                if (++i == items.length)
                    i = 0; 
            } while (i != putIndex);//繼承查詢
        }
        return false;
    } finally {
        lock.unlock();
    }
}

//根據索引刪除元素,實際上是把刪除索引之後的元素往前移動一個位置
void removeAt(final int removeIndex) {

    final Object[] items = this.items;
    //先判斷要刪除的元素是否為當前佇列頭元素
    if (removeIndex == takeIndex) {
      //如果是直接刪除
      items[takeIndex] = null;
      //當前佇列頭元素加1並判斷是否與陣列長度相等,若為true設定為0
      if (++takeIndex == items.length)
          takeIndex = 0;
      count--;//佇列元素減1
      if (itrs != null)
          itrs.elementDequeued();//更新迭代器中的資料
    } else {
    //如果要刪除的元素不在佇列頭部,
    //那麼只需迴圈迭代把刪除元素後面的所有元素往前移動一個位置
      //獲取下一個要被新增的元素的索引,作為迴圈判斷結束條件
      final int putIndex = this.putIndex;
      //執行迴圈
      for (int i = removeIndex;;) {
          //獲取要刪除節點索引的下一個索引
          int next = i + 1;
          //判斷是否已為陣列長度,如果是從陣列頭部(索引為0)開始找
          if (next == items.length)
              next = 0;
           //如果查詢的索引不等於要新增元素的索引,說明元素可以再移動
          if (next != putIndex) {
              items[i] = items[next];//把後一個元素前移覆蓋要刪除的元
              i = next;
          } else {
          //在removeIndex索引之後的元素都往前移動完畢後清空最後一個元素
              items[i] = null;
              this.putIndex = i;
              break;//結束迴圈
          }
      }
      count--;//佇列元素減1
      if (itrs != null)
          itrs.removedAt(removeIndex);//更新迭代器資料
    }
    notFull.signal();//喚醒新增執行緒
}

take方法其實很簡單,有就刪除沒有就阻塞,只不過這個阻塞是可以中斷的,如果佇列沒有資料那麼就加入notEmpty條件佇列等待(有資料就直接取走,方法結束),如果有新的put執行緒新增了資料,那麼put操作將會喚醒take執行緒,執行take操作。

//從佇列頭部刪除,佇列沒有元素就阻塞,可中斷
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();//中斷
    try {
      //如果佇列沒有元素
      while (count == 0)
          //執行阻塞操作
          notEmpty.await();
      return dequeue();//如果佇列有元素執行刪除操作
    } finally {
      lock.unlock();
    }
}

peek方法直接返回當前佇列的頭元素但不刪除任何元素。

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    //直接返回當前佇列的頭元素,但不刪除
      return itemAt(takeIndex); // null when queue is empty
    } finally {
      lock.unlock();
    }
}

final E itemAt(int i) {
    return (E) items[i];
}

LinkedBlockingQueue

LinkedBlockingQueue是一個基於連結串列的阻塞佇列,其內部維持一個基於連結串列的資料佇列,使用兩把鎖(takeLock,putLock)允許讀寫並行,remove和iterator時需要同時獲取兩把鎖。

LinkedBlockingQueue預設為無界佇列,即大小為Integer.MAX_VALUE,如果消費者速度慢於生產者速度,可能造成記憶體空間不足,建議手動設定佇列大小。

 public class LinkedBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
    
        /**
         * 節點類,用於儲存資料
         */
        static class Node<E> {
            E item;
            Node<E> next;
            Node(E x) { item = x; }
        }
    
        /** 阻塞佇列的大小,預設為Integer.MAX_VALUE */
        private final int capacity;
    
        /** 當前阻塞佇列中的元素個數 */
        private final AtomicInteger count = new AtomicInteger();
    
        /** 阻塞佇列的頭結點 */
        transient Node<E> head;
    
        /** 阻塞佇列的尾節點 */
        private transient Node<E> last;
    
        /** 獲取並移除元素時使用的鎖,如take, poll, etc */
        private final ReentrantLock takeLock = new ReentrantLock();
    
        /** notEmpty條件物件,當佇列沒有資料時用於掛起執行刪除的執行緒 */
        private final Condition notEmpty = takeLock.newCondition();
    
        /** 新增元素時使用的鎖如 put, offer, etc */
        private final ReentrantLock putLock = new ReentrantLock();
    
        /** notFull條件物件,當佇列資料已滿時用於掛起執行新增的執行緒 */
        private final Condition notFull = putLock.newCondition();
    
    }

每次插入操作都將動態構造Linked nodes。

每個新增到LinkedBlockingQueue佇列中的資料都將被封裝成Node節點,新增的連結串列佇列中,其中head和last分別指向佇列的頭結點和尾結點。

與ArrayBlockingQueue不同的是,LinkedBlockingQueue內部分別使用了takeLock 和 putLock 對併發進行控制,也就是說,新增和刪除操作並不是互斥操作,可以同時進行,這樣也就可以大大提高吞吐量。

建構函式

1、LinkedBlockingQueue():初始化容量為Integer.MAX_VALUE的佇列;
2、LinkedBlockingQueue(int capacity):指定佇列容量並初始化頭尾節點
3、LinkedBlockingQueue(Collection c):初始化一個容量為Integer.MAX_VALUE且包含集合c所有元素的佇列,且阻塞佇列的迭代順序同集合c。若集合c元素包含null,將throwNullPointerException;若集合c元素個數達到Integer.MAX_VALUE,將throwIllegalStateException(“Queue full”)。

// 將node連結到佇列尾部
private void enqueue(Node<E> node) { // 入隊
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    last = last.next = node; // 等價於last.next = node;last = last.next(即node)
}
public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended(競爭), but necessary for visibility(可見性)
    try {
        int n = 0;
        for (E e : c) {
            if (e == null)
                throw new NullPointerException();
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e)); // 執行last = last.next = node;
            ++n;
        }
        count.set(n); // 設定佇列元素個數
    } finally {
        putLock.unlock();
    }
}

新增元素阻塞

新增元素主要有這幾個方法add、offer、put

   public boolean add(E e) {
     if (offer(e))
         return true;
     else
         throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
    //新增元素為null直接丟擲異常
    if (e == null) throw new NullPointerException();
    //獲取佇列的個數
    final AtomicInteger count = this.count;
    //判斷佇列是否已滿
    if (count.get() == capacity)
      return false;
    int c = -1;
    //構建節點
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
      //再次判斷佇列是否已滿,考慮併發情況
      if (count.get() < capacity) {
          enqueue(node);//新增元素
          c = count.getAndIncrement();//拿到當前未新增新元素時的佇列長度
          //如果容量還沒滿
          if (c + 1 < capacity)
              notFull.signal();//喚醒下一個新增執行緒,執行新增操作
      }
    } finally {
      putLock.unlock();
    }
    // 由於存在新增鎖和消費鎖,而消費鎖和新增鎖都會持續喚醒等到執行緒,因此count肯定會變化。
    //這裡的if條件表示如果佇列中還有1條資料
    if (c == 0) 
    signalNotEmpty();//如果還存在資料那麼就喚醒消費鎖
    return c >= 0; // 新增成功返回true,否則返回false
}

//入隊操作
private void enqueue(Node<E> node) {
    //佇列尾節點指向新的node節點
    last = last.next = node;
}

//signalNotEmpty方法
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
      //喚醒獲取並刪除元素的執行緒
      notEmpty.signal();
    } finally {
      takeLock.unlock();
    }
}

這裡的Offer()方法做了兩件事:

  • 判斷佇列是否滿,滿了就直接釋放鎖,沒滿就將節點封裝成Node入隊,然後再次判斷佇列新增完成後是否已滿,不滿就繼續喚醒等到在條件物件notFull上的新增執行緒。
  • 判斷是否需要喚醒等到在notEmpty條件物件上的消費執行緒。

為什麼新增完成後是繼續喚醒在條件物件notFull上的新增執行緒而不是像ArrayBlockingQueue那樣直接喚醒notEmpty條件物件上的消費執行緒?為什麼要當if (c == 0)時才去喚醒消費執行緒呢?

第一個疑問:在新增新元素完成後,會判斷佇列是否已滿,不滿就繼續喚醒在條件物件notFull上的新增執行緒,這點與前面分析的ArrayBlockingQueue很不相同,在ArrayBlockingQueue內部完成新增操作後,會直接喚醒消費執行緒對元素進行獲取,這是因為ArrayBlockingQueue只用了一個ReenterLock同時對新增執行緒和消費執行緒進行控制,這樣如果在新增完成後再次喚醒新增執行緒的話,消費執行緒可能永遠無法執行。

而對於LinkedBlockingQueue來說就不一樣了,其內部對新增執行緒和消費執行緒分別使用了各自的ReenterLock鎖對併發進行控制,也就是說新增執行緒和消費執行緒是不會互斥的,所以新增鎖只要管好自己的新增執行緒即可,新增執行緒自己直接喚醒自己的其他新增執行緒,如果沒有等待的新增執行緒,直接結束了。

如果有就直到佇列元素已滿才結束掛起,當然offer方法並不會掛起,而是直接結束,只有put方法才會當佇列滿時才執行掛起操作。注意消費執行緒的執行過程也是如此。這也是為什麼LinkedBlockingQueue的吞吐量要相對大些的原因。

第二個疑問:消費執行緒一旦被喚醒是一直在消費的(前提是有資料),所以c值是一直在變化的,c值是新增完元素前佇列的大小,此時c只可能是0或c>0,如果是c=0,那麼說明之前消費執行緒已停止,條件物件上可能存在等待的消費執行緒,新增完資料後應該是c+1,那麼有資料就直接喚醒等待消費執行緒,如果沒有就結束啦,等待下一次的消費操作。如果c>0那麼消費執行緒就不會被喚醒,只能等待下一個消費操作(poll、take、remove)的呼叫,那為什麼不是條件c>0才去喚醒呢?我們要明白的是消費執行緒一旦被喚醒會和新增執行緒一樣,一直不斷喚醒其他消費執行緒,如果新增前c>0,那麼很可能上一次呼叫的消費執行緒後,資料並沒有被消費完,條件佇列上也就不存在等待的消費執行緒了,所以c>0喚醒消費執行緒得意義不是很大,當然如果新增執行緒一直新增元素,那麼一直c>0,消費執行緒執行的換就要等待下一次呼叫消費操作了(poll、take、remove)。

根據時間阻塞

//在指定時間內阻塞新增的方法,超時就結束
 public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

    if (e == null) throw new NullPointerException();
    //將時間轉換成納秒
    long nanos = unit.toNanos(timeout);
    int c = -1;
    //獲取鎖
    final ReentrantLock putLock = this.putLock;
    //獲取當前佇列大小
    final AtomicInteger count = this.count;
    //鎖中斷(如果需要)
    putLock.lockInterruptibly();
    try {
        //判斷佇列是否滿
        while (count.get() == capacity) {
            if (nanos <= 0)
                return false;
            //如果佇列滿根據阻塞的等待
            nanos = notFull.awaitNanos(nanos);
        }
        //佇列沒滿直接入隊
        enqueue(new Node<E>(e));
        c = count.getAndIncrement();
        //喚醒條件物件上等待的執行緒
        if (c + 1 < capacity)
            notFull.signal();
    } finally { 
        putLock.unlock();
    }
    //喚醒消費執行緒
    if (c == 0)
        signalNotEmpty();
    return true;
}

//CoditionObject(Codition的實現類)中的awaitNanos方法
 public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //這裡是將當前新增執行緒封裝成NODE節點加入Condition的等待佇列中
    //注意這裡的NODE是AQS的內部類Node
    Node node = addConditionWaiter();
    //加入等待,那麼就釋放當前執行緒持有的鎖
    int savedState = fullyRelease(node);
    //計算過期時間
    final long deadline = System.nanoTime() + nanosTimeout;
    int interruptMode = 0;

    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            transferAfterCancelledWait(node);
            break;
        }
        //主要看這裡!!由於是while 迴圈,這裡會不斷判斷等待時間
        //nanosTimeout 是否超時
        //static final long spinForTimeoutThreshold = 1000L;
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);//掛起執行緒
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        //重新計算剩餘等待時間,while迴圈中繼續判斷下列公式
        //nanosTimeout >= spinForTimeoutThreshold
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return deadline - System.nanoTime();
}

據傳遞進來的時間計算超時阻塞nanosTimeout,然後通過while迴圈中判斷nanosTimeout >= spinForTimeoutThreshold 該公式是否成立,當其為true時則說明超時時間nanosTimeout 還未到期,再次計算nanosTimeout = deadline - System.nanoTime();即nanosTimeout ,持續判斷,直到nanosTimeout 小於spinForTimeoutThreshold結束超時阻塞操作,方法也就結束。

這裡的spinForTimeoutThreshold其實更像一個經驗值,因為非常短的超時等待無法做到十分精確,因此採用了spinForTimeoutThreshold這樣一個臨界值。offer(E e, long timeout, TimeUnit unit)方法內部正是利用這樣的Codition的超時等待awaitNanos方法實現新增方法的超時阻塞操作。同樣對於poll(long timeout, TimeUnit unit)方法也是一樣的道理。

移除元素阻塞

移除的方法主要有remove、poll、take

remove方法刪除指定的物件。由於remove方法刪除的資料的位置不確定,為了避免造成並非安全問題,所以需要同時對putLock和takeLock加鎖。

public boolean remove(Object o) {
    if (o == null) return false;
    fullyLock();//同時對putLock和takeLock加鎖
    try {
        //迴圈查詢要刪除的元素
        for (Node<E> trail = head, p = trail.next;
          p != null;
          trail = p, p = p.next) {
         if (o.equals(p.item)) {//找到要刪除的節點
             unlink(p, trail);//直接刪除
             return true;
         }
        }
        return false;
    } finally {
     fullyUnlock();//解鎖
    }
}

//兩個同時加鎖
void fullyLock() {
    putLock.lock();
    takeLock.lock();
}

void fullyUnlock() {
    takeLock.unlock();
    putLock.unlock();
}

poll方法比較簡單,如果佇列沒有資料就返回null,如果佇列有資料,那麼就取出來,如果佇列還有資料那麼喚醒等待在條件物件notEmpty上的消費執行緒。然後判斷if (c == capacity)為true就喚醒新增執行緒,這點與前面分析if(c==0)是一樣的道理。因為只有可能佇列滿了,notFull條件物件上才可能存在等待的新增執行緒。

public E poll() {
    //獲取當前佇列的大小
    final AtomicInteger count = this.count;
    if (count.get() == 0)//如果沒有元素直接返回null
        return null;
    E x = null;
    int c = -1;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        //判斷佇列是否有資料
        if (count.get() > 0) {
            //如果有,直接刪除並獲取該元素值
            x = dequeue();
            //當前佇列大小減一
            c = count.getAndDecrement();
            //如果佇列未空,繼續喚醒等待在條件物件notEmpty上的消費執行緒
            if (c > 1)
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }
    //判斷c是否等於capacity,這是因為如果滿說明NotFull條件物件上
    //可能存在等待的新增執行緒
    if (c == capacity)
        signalNotFull();
    return x;
}

private E dequeue() {
    Node<E> h = head;//獲取頭結點
    Node<E> first = h.next; 獲取頭結的下一個節點(要刪除的節點)
    h.next = h; // help GC//自己next指向自己,即被刪除
    head = first;//更新頭結點
    E x = first.item;//獲取刪除節點的值
    first.item = null;//清空資料,因為first變成頭結點是不能帶資料的,這樣也就刪除佇列的帶資料的第一個節點
    return x;
}

take方法是一個可阻塞可中斷的移除方法主要做了兩件事

如果佇列沒有資料就掛起當前執行緒到 notEmpty條件物件的等待佇列中一直等待,如果有資料就刪除節點並返回資料項,同時喚醒後續消費執行緒,嘗試喚醒條件物件notFull上等待佇列中的新增執行緒。

移除方法中只有take方法具備阻塞功能。remove方法是成功返回true失敗返回false,poll方法成功返回被移除的值,失敗或沒資料返回null。

public E take() throws InterruptedException {
    E x;
    int c = -1;
    //獲取當前佇列大小
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();//可中斷
    try {
        //如果佇列沒有資料,掛機當前執行緒到條件物件的等待佇列中
        while (count.get() == 0) {
            notEmpty.await();
        }
        //如果存在資料直接刪除並返回該資料
        x = dequeue();
        c = count.getAndDecrement();//佇列大小減1
        if (c > 1)
            notEmpty.signal();//還有資料就喚醒後續的消費執行緒
    } finally {
        takeLock.unlock();
    }
    //滿足條件,喚醒條件物件上等待佇列中的新增執行緒
    if (c == capacity)
        signalNotFull();
    return x;
}

lock 與 lockInterruptibly的區別

lock優先考慮獲取鎖,待獲取鎖成功後,才響應中斷。

lockInterruptibly 優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取。

詳細區別:

ReentrantLock.lockInterruptibly允許在等待時由其它執行緒呼叫等待執行緒的Thread.interrupt方法來中斷等待執行緒的等待而直接返回,這時不用獲取鎖,而會丟擲一個InterruptedException。

ReentrantLock.lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。只是在最後獲取鎖成功後再把當前執行緒置為interrupted狀態,然後再中斷執行緒。

執行緒喚醒
對notEmpty和notFull的喚醒操作均使用的是signal()而不是signalAll()。

signalAll() 雖然能喚醒Condition上所有等待的執行緒,但卻並不見得會節省資源,相反,喚醒操作會帶來上下文切換,且會有鎖的競爭。此外,由於此處獲取的鎖均是同一個(putLock或takeLock),同一時刻被鎖的執行緒只有一個,也就無從談起喚醒多個執行緒了。

LinkedBlockingQueue與ArrayBlockingQueue比較
ArrayBlockingQueue底層基於陣列,建立時必須指定佇列大小,“有界”LinkedBlockingQueue“無界”,節點動態建立,節點出隊後可被GC,故伸縮性較好;

ArrayBlockingQueue入隊和出隊使用同一個lock(但資料讀寫操作已非常簡潔),讀取和寫入操作無法並行,LinkedBlockingQueue使用雙鎖可並行讀寫,其吞吐量更高。

ArrayBlockingQueue在插入或刪除元素時直接放入陣列指定位置(putIndex、takeIndex),不會產生或銷燬任何額外的物件例項;而LinkedBlockingQueue則會生成一個額外的Node物件,在高效併發處理大量資料時,對GC的影響存在一定的區別。

相關文章