多線程基礎總結十--LinkedBlockingQueue

轉:http://yanxuxin.javaeye.com/blog/582162


隨着多線程基礎總結的增多,卻明顯的感覺知道的越來越少,好像轉了一圈又回到了什麼都不懂的起點。不過還是試着介紹一下隊列的併發實現,努力盡快的驅散迷 霧。隊列這個數據結構已經很熟悉了,利用其先進先出的特性,多數生產消費模型的首選數據結構就是隊列。對於有多個生產者和多個消費者線程的模型來說,最重 要是他們共同訪問的Queue是線程安全的。JDK中提供的線程安全的Queue的實現還是很豐富 的:ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,DelayQueue,ConcurrentLinkedQueue 等等,多數情況下使用這些數據結構編寫併發程序足夠了。ArrayBlockingQueue的實現之前的總結中已經有介紹,所以這次是分析一下 LinkedBlockingQueue和ConcurrentLinkedQueue的源碼實現。

    首先從簡單的開始,先看看LinkedBlockingQueue線程安全的實現。之所以介紹它是因爲其實現比較典型,對比 ArrayBlokcingQueue使用一個ReentrantLock和兩個Condition維護內部的數組來說,它使用了兩個 ReentrantLock,並且分別對應一個Condition來實現對內部數據結構Node型變量的維護。

Java代碼
  1. public   class  LinkedBlockingQueue<E>  extends  AbstractQueue<E>  
  2.         implements  BlockingQueue<E>, java.io.Serializable {  
  3.     private   static   final   long  serialVersionUID = -6903933977591709194L;  
  4.   
  5.     /**  
  6.      * 節點數據結構  
  7.      */   
  8.     static   class  Node<E> {  
  9.         /** The item, volatile to ensure barrier separating write and read */   
  10.         volatile  E item;  
  11.         Node<E> next;  
  12.         Node(E x) { item = x; }  
  13.     }  
  14.   
  15.     /** 隊列的容量 */   
  16.     private   final   int  capacity;  
  17.   
  18.     /** 持有節點計數器 */   
  19.     private   final  AtomicInteger count =  new  AtomicInteger( 0 );  
  20.   
  21.     /** 頭指針 */   
  22.     private   transient  Node<E> head;  
  23.   
  24.     /** 尾指針 */   
  25.     private   transient  Node<E> last;  
  26.   
  27.     /** 用於讀取的獨佔鎖*/   
  28.     private   final  ReentrantLock takeLock =  new  ReentrantLock();  
  29.   
  30.     /** 隊列是否爲空的條件 */   
  31.     private   final  Condition notEmpty = takeLock.newCondition();  
  32.   
  33.     /** 用於寫入的獨佔鎖 */   
  34.     private   final  ReentrantLock putLock =  new  ReentrantLock();  
  35.   
  36.     /** 隊列是否已滿的條件 */   
  37.     private   final  Condition notFull = putLock.newCondition();  
  38.   
  39.     private   void  signalNotEmpty() {  
  40.         final  ReentrantLock takeLock =  this .takeLock;  
  41.         takeLock.lock();  
  42.         try  {  
  43.             notEmpty.signal();  
  44.         } finally  {  
  45.             takeLock.unlock();  
  46.         }  
  47.     }  
  48.   
  49.     private   void  signalNotFull() {  
  50.         final  ReentrantLock putLock =  this .putLock;  
  51.         putLock.lock();  
  52.         try  {  
  53.             notFull.signal();  
  54.         } finally  {  
  55.             putLock.unlock();  
  56.         }  
  57.     }  
  58.   
  59.     private   void  insert(E x) {  
  60.         last = last.next = new  Node<E>(x);  
  61.     }  
  62.   
  63.     private  E extract() {  
  64.         Node<E> first = head.next;  
  65.         head = first;  
  66.         E x = first.item;  
  67.         first.item = null ;  
  68.         return  x;  
  69.     }  
  70.   
  71.     private   void  fullyLock() {  
  72.         putLock.lock();  
  73.         takeLock.lock();  
  74.     }  
  75.   
  76.     private   void  fullyUnlock() {  
  77.         takeLock.unlock();  
  78.         putLock.unlock();  
  79.     }  
  80.   
  81.     public  LinkedBlockingQueue( int  capacity) {  
  82.         if  (capacity <=  0throw   new  IllegalArgumentException();  
  83.         this .capacity = capacity;  
  84.         last = head = new  Node<E>( null );  
  85.     }  
  86.    ...  
  87. }  
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;

    /**
     * 節點數據結構
     */
    static class Node<E> {
        /** The item, volatile to ensure barrier separating write and read */
        volatile E item;
        Node<E> next;
        Node(E x) { item = x; }
    }

    /** 隊列的容量 */
    private final int capacity;

    /** 持有節點計數器 */
    private final AtomicInteger count = new AtomicInteger(0);

    /** 頭指針 */
    private transient Node<E> head;

    /** 尾指針 */
    private transient Node<E> last;

    /** 用於讀取的獨佔鎖*/
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 隊列是否爲空的條件 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 用於寫入的獨佔鎖 */
    private final ReentrantLock putLock = new ReentrantLock();

    /** 隊列是否已滿的條件 */
    private final Condition notFull = putLock.newCondition();

    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

    private void insert(E x) {
        last = last.next = new Node<E>(x);
    }

    private E extract() {
        Node<E> first = head.next;
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

    private void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }

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

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
   ...
}


這裏僅僅展示部分源碼,主要的方法在後面的分析中列出。分析之前明確一個最基本的概念。天天唸叨着編寫線程安全的類,什麼是線程安全的類?那就是 類內共享的全局變量的訪問必須保證是不受多線程形式影響的。如果由於多線程的訪問(改變,遍歷,查看)而使這些變量結構被破壞或者針對這些變量操作的原子 性被破壞,則這個類的編寫不是線程安全的。

    明確了這個基本的概念就可以很好的理解這個Queue的實現爲什麼是線程安全的了。在LinkedBlockingQueue的所有共享的全局變量 中,final聲明的capacity在構造器生成實例時就成了不變量了。而final聲明的count由於是AtomicInteger類型的,所以能 夠保證其操作的原子性。剩下的final的變量都是初始化成了不變量,並且不包含可變屬性,所以都是訪問安全的。那麼剩下的就是Node類型的head和 last兩個可變量。所以要保證LinkedBlockingQueue是線程安全的就是要保證對head和last的訪問是線程安全的

    首先從上面的源碼可以看到insert(E x),extract()是真正的操作head,last來入隊和出對的方法,但是由於是私有的,所以不能被直接訪問,不用擔心線程的問題。實際入隊的公 開的方法是put(E e),offer(E e)和offer(E e, long timeout, TimeUnit unit)。put(...)方法與offer(...)都是把新元素加入到隊尾,所不同的是如果不滿足條件put會把當前執行的線程扔到等待集中等待被 喚醒繼續執行,而offer則是直接退出,所以如果是需要使用它的阻塞特性的話,不能直接使用poll(...)。
   
    put(...)方法中加入元素的操作使用this.putLock來限制多線程的訪問,並且使用了可中斷的方式:

Java代碼
  1. public   void  put(E e)  throws  InterruptedException {  
  2.         if  (e ==  nullthrow   new  NullPointerException();  
  3.         int  c = - 1 ;  
  4.         final  ReentrantLock putLock =  this .putLock;  
  5.         final  AtomicInteger count =  this .count;  //----------------a   
  6.         putLock.lockInterruptibly();//隨時保證響應中斷 //--------b   
  7.         try  {  
  8.             //*****************************(1)*********************************   
  9.             try  {  
  10.                 while  (count.get() == capacity)  
  11.                     notFull.await();  
  12.             } catch  (InterruptedException ie) {  
  13.                 notFull.signal(); // propagate to a non-interrupted thread   
  14.                 throw  ie;  
  15.             }  
  16.            //*****************************end*********************************   
  17.             insert(e);//真正的入隊操作   
  18.            //********************(2)**********************   
  19.             c = count.getAndIncrement();  
  20.             if  (c +  1  < capacity)  
  21.                 notFull.signal();  
  22.             //******************end**********************   
  23.         } finally  {  
  24.             putLock.unlock();  
  25.         } //-------------------------c   
  26.         if  (c ==  0//---------------d   
  27.             signalNotEmpty();  
  28. }  
public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count; //----------------a
        putLock.lockInterruptibly();//隨時保證響應中斷 //--------b
        try {
            //*****************************(1)*********************************
            try {
                while (count.get() == capacity)
                    notFull.await();
            } catch (InterruptedException ie) {
                notFull.signal(); // propagate to a non-interrupted thread
                throw ie;
            }
           //*****************************end*********************************
            insert(e);//真正的入隊操作
           //********************(2)**********************
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
            //******************end**********************
        } finally {
            putLock.unlock();
        } //-------------------------c
        if (c == 0) //---------------d
            signalNotEmpty();
}


代碼段(1)是阻塞操作,代碼段(2)是count遞增和喚醒等待的操作。兩者之間的insert(e)纔是入隊操作,其實際是操作的隊尾引用last,並且沒有牽涉到head 。 所以設計兩個鎖的原因就在這裏!因爲出隊操作take(),poll()實際是執行extract()僅僅操作隊首引用head。增加了 this.takeLock這個鎖,就實現了多個不同任務的線程入隊的同時可以進行出對的操作,並且由於兩個操作所共同使用的count是 AtomicInteger類型的,所以完全不用考慮計數器遞增遞減的問題。假設count換成int,則相應的putLock內的count++和 takeLock內的count--有可能相互覆蓋,最終造成count的值被腐蝕,故這種設計必須使用原子操作類。
      我之前說過,保證類的線程安全只要保證head和last的操作的線程安全,也就是保證insert(E x)和extract()線程安全即可。那麼上面的put方法中的代碼段(1)放在a,b之間,代碼段(2)放在c,d之間不是更好?畢竟鎖的粒度越小越 好。單純的考慮count的話這樣的改變是正確的,但是await()和singal()這兩個方法執行時都會檢查當前線程是否是獨佔鎖的那個線程,如果不是則拋出java.lang.IllegalMonitorStateException異常 。而這兩段代碼中包含notFull.await()和notFull.signal()這兩句使得(1),(2)必須放在lock保護塊內。這裏說明主要是count本身並不需要putLock或者takeLock的保護,從

Java代碼
  1. public   int  size() {  
  2.         return  count.get();  
  3. }  
public int size() {
        return count.get();
}


可以看出count的訪問是不需要任何鎖的。而在put等方法中,其與鎖機制的混用很容易造成迷惑。最後put中的代碼d的作用主要是一個低位及 時通知的作用,也就是隊列剛有值試圖獲得takeLock去通知等待集中的出隊線程。因爲c==0意味着count.getAndIncrement() 原子遞增成功,所以count > 0成立。類似作用的代碼:

Java代碼
  1. if  (c == capacity)  
  2.        signalNotFull();  
if (c == capacity)
       signalNotFull();


在take和poll中也有出現,實現了高位及時通知。

    分析完了put,對應的offer,take,poll方法都是類似的實現。下面看看遍歷隊列的操作:

Java代碼
  1. public  Object[] toArray() {  
  2.         fullyLock();  
  3.         try  {  
  4.             int  size = count.get();  
  5.             Object[] a = new  Object[size];  
  6.             int  k =  0 ;  
  7.             for  (Node<E> p = head.next; p !=  null ; p = p.next)  
  8.                 a[k++] = p.item;  
  9.             return  a;  
  10.         } finally  {  
  11.             fullyUnlock();  
  12.         }  
  13. }  
public Object[] toArray() {
        fullyLock();
        try {
            int size = count.get();
            Object[] a = new Object[size];
            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next)
                a[k++] = p.item;
            return a;
        } finally {
            fullyUnlock();
        }
}


這個方法很簡單主要是要清楚一點:這個操作執行時不允許其他線程再修改隊首和隊尾,所以使用了fullyLock去獲取putLock和takeLock,只要成功則可以保證不會再有修改隊列的操作。然後就是安心的遍歷到最後一個元素爲止了。

    另外在offer(E e, long timeout, TimeUnit unit)這個方法中提供了帶有超時的入隊操作,如果一直不成功的話,它會嘗試在timeout的時間內入隊:

Java代碼
  1. for  (;;) {  
  2.      ...//入隊操作   
  3.      if  (nanos <=  0 )  
  4.          return   false ;  
  5.      try  {  
  6.           nanos = notFull.awaitNanos(nanos);  
  7.      } catch  (InterruptedException ie) {  
  8.            notFull.signal(); // propagate to a non-interrupted thread   
  9.            throw  ie;  
  10.      }  
  11. }  
for (;;) {
     ...//入隊操作
     if (nanos <= 0)
         return false;
     try {
          nanos = notFull.awaitNanos(nanos);
     } catch (InterruptedException ie) {
           notFull.signal(); // propagate to a non-interrupted thread
           throw ie;
     }
}


其內部循環使用notFull.awaitNanos(nanos)方法反覆的計算剩餘時間的大概值用於實現延時功能。nanos<=0則放棄嘗試,直接退出。

    整體而言,LinkedBlockingQueue的實現還是很清晰的。相對於後面要介紹的ConcurrentLinkedQueue來說,它屬於簡單 的實現。這些看似複雜的數據結構的實現實質都是多線程的基礎的綜合應用。就好像數學中千變萬化的難題其實都是基礎公式的組合一樣,如果有清晰的基礎認知, 還是能找到自己分析的思路的。本來是想從mina中找找類似的實現,不過很遺憾的是它好像僅僅實現了一個非線程安全的循環隊列,然後在其基礎上使用 synchronized進行封裝成線程安全的Queue。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章