轉:http://yanxuxin.javaeye.com/blog/582162
隨着多線程基礎總結的增多,卻明顯的感覺知道的越來越少,好像轉了一圈又回到了什麼都不懂的起點。不過還是試着介紹一下隊列的併發實現,努力盡快的驅散迷
霧。隊列這個數據結構已經很熟悉了,利用其先進先出的特性,多數生產消費模型的首選數據結構就是隊列。對於有多個生產者和多個消費者線程的模型來說,最重
要是他們共同訪問的Queue是線程安全的。JDK中提供的線程安全的Queue的實現還是很豐富
的:ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,DelayQueue,ConcurrentLinkedQueue
等等,多數情況下使用這些數據結構編寫併發程序足夠了。ArrayBlockingQueue的實現之前的總結中已經有介紹,所以這次是分析一下
LinkedBlockingQueue和ConcurrentLinkedQueue的源碼實現。
首先從簡單的開始,先看看LinkedBlockingQueue線程安全的實現。之所以介紹它是因爲其實現比較典型,對比
ArrayBlokcingQueue使用一個ReentrantLock和兩個Condition維護內部的數組來說,它使用了兩個
ReentrantLock,並且分別對應一個Condition來實現對內部數據結構Node型變量的維護。
- 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 );
- }
- ...
- }
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來限制多線程的訪問,並且使用了可中斷的方式:
- 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();
- }
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的保護,從
- public int size() {
- return count.get();
- }
public int size() {
return count.get();
}
可以看出count的訪問是不需要任何鎖的。而在put等方法中,其與鎖機制的混用很容易造成迷惑。最後put中的代碼d的作用主要是一個低位及
時通知的作用,也就是隊列剛有值試圖獲得takeLock去通知等待集中的出隊線程。因爲c==0意味着count.getAndIncrement()
原子遞增成功,所以count > 0成立。類似作用的代碼:
- if (c == capacity)
- signalNotFull();
if (c == capacity)
signalNotFull();
在take和poll中也有出現,實現了高位及時通知。
分析完了put,對應的offer,take,poll方法都是類似的實現。下面看看遍歷隊列的操作:
- 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();
- }
- }
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的時間內入隊:
- for (;;) {
- ...//入隊操作
- if (nanos <= 0 )
- return false ;
- try {
- nanos = notFull.awaitNanos(nanos);
- } catch (InterruptedException ie) {
- notFull.signal(); // propagate to a non-interrupted thread
- throw ie;
- }
- }
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。