並行化資源池隊列 1 —— 部分有界隊列
1前言
在併發系統中很多地方都要用到作爲資源池的並行化隊列,如在大多數應用中,一個或多個生產者線程生產數據,一個或多個消費者消費數據。這些數據元素可以是需要執行的的任務、要解釋的鍵盤輸入、待處理的訂單或需要解碼的數據包。有的時候生產者會突然加速,產生數據的速度會遠遠超過消費者使用數據的速度。爲了使消費者可以跟得上生產者,需要生產者和消費者之間放置一個緩衝區,將那些來不及處理的數據先放在緩衝區中,以使得他們能被儘快的消費。此時多會採用隊列作爲池,來實現生產者和消費者之間的緩衝區。通常來說池有以下幾種不同的變化方式:
Ø 池可以是有界的或者是無界的。有界池存放有限個元素,該界限稱爲池的容量。無界池可以存放任意數量的元素。當要求生產者不需要過快的超過消費者時,即生產和消費是一種鬆弛的同步,就要用到有界池。反之當不需要設置固定的界限,限制生產者可比消費者快多少的程度時,就要用到無界池。
Ø 操作池的方法可以是完全、部分或同步的。
u 若一個方法不需要等待某個條件成立,則稱爲該方法是完全的。如從空隊列中刪除元素時,可以立刻拋出返回錯誤。因此當生產者或消費者線程有比等待調用生效還要好的其他事情可處理時,完全化的操作接口非常適用。
u 若一個方法的調用需要等待某個條件成立,則稱爲該方法爲部分的。如從空隊列中刪除元素的操作會被阻塞,直到池中有可用元素才返回。當生產者或消費者線程除了等待調用生效以外,沒有其他更好的事情可做時,部分化的操作接口非常適用。
u 若一個方法需要等待另一個方法與他的調用相重疊,則稱該方法爲同步的。如一個向隊列中添加元素的方法調用會被阻塞,直到被添加的元素被另一個線程的方法取用。當生產者和消費者線程之間需要進行通信或嚴格同步時,同步化的操作接口非常適用。
池可以基於各種數據結構來實現,進而實現各種公平策略。如基於隊列的先進先出的池,基於棧的後進先出的池,以及其他的一些若公平性原則的池。但無論如何實現池的公平策略,都要解決在高並行環境下高性能、低消耗、無干擾的交互,事實上要達到這樣級別的並行性並非易事。下面我們將分爲部分有界隊列池、完全無界隊列池、以及同步化隊列池幾種情況,來探討並行環境下基於隊列的池,如何實現高級別並行的相關技術細節。
2部分有界隊列
下面的代碼基於鏈表實現有界隊列,並且使用Java5之後提供的原子變量、可重入鎖、條件變量等手段實現並行環境下的線程交互控制。通過原子變量、可重入鎖、條件變量等技術手段,可以實現更加細化的鎖粒度的控制,相比之前Java提供的同步鎖機制,這樣實現能夠實現更加高效的線程交互和更小的總體消耗。
首先定義基於鏈表的隊列的元素節點對象:
public class BoundedNode {
public Object value;
public BoundedNode next;
public BoundedNode(Object x){
value=x;
next=null;
}
}
然後定義隊列的主體實現,其中主要包括:入隊和出對操作。
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue {
ReentrantLock enqlock,deqlock;
Condition notempty,notfull;
AtomicInteger size;
BoundedNode head,tail;//隊列頭哨兵和尾哨兵
intcapacity;
publicBoundedQueue(intcapacity){
this.capacity=capacity;
head=new BoundedNode(null);
tail=head;
size=new AtomicInteger(0);
enqlock=new ReentrantLock();
deqlock=new ReentrantLock();
notfull=enqlock.newCondition();
notempty=deqlock.newCondition();
}
public void enq(Object x) throws InterruptedException{
boolean weakdeq=false;
//入隊者首先獲取入隊鎖
enqlock.lock();
try{
//判斷隊列是否爲滿,通過循環判斷,結合上面的加鎖,因此此方法也稱爲自旋
//加鎖,優勢效率較高,缺點造成CPU消耗較大
while(size.get()==capacity){
//如果隊列滿,則在“不滿”條件上等待,直到隊列不滿的條件發生,等待時會
//暫時釋放入隊鎖
notfull.await();
}
//如果“不滿”條件滿足,則構建新的隊列元素,並將新的隊列元素掛接到隊列尾部
BoundedNode e=new BoundedNode(x);
tail.next=tail=e;
//獲取元素入隊前隊列容量,並在獲取後將入隊前隊列容量增加1
if(size.getAndIncrement()==0){
//如果入隊前隊列容量等於0,則說明有出隊線程正在等待出隊條件notempty
//發生,因此要將相關標誌置爲true
weakdeq=true;
}
}finally{
//入隊者釋放入隊鎖
enqlock.unlock();
}
//判斷出隊等待標誌
if(weakdeq){
//入隊線程獲取出隊鎖
deqlock.lock();
try{
//觸發出隊條件,即隊列“不空”條件,使等待出隊的線程能夠繼續執行
notempty.signalAll();
}finally{
//入隊線程釋放出隊鎖
deqlock.unlock();
}
}
}
publicObject deq() throwsInterruptedException{
Object result=null;
boolean weakenq=false;
//出隊者首先獲取出隊鎖
deqlock.lock();
try{
//判斷隊列是否爲空,通過循環判斷,結合上面的加鎖,因此此方法也稱爲自旋加鎖,
//優勢效率較高,缺點造成CPU消耗較大
while(size.get()==0){
//如果隊列空,則在“不空”條件上等待,//
直到隊列不空的條件發生,等待時會暫時釋放出隊鎖
notempty.await();
}
//如果“不空”條件滿足,則通過隊列頭部哨兵獲取首節點,並獲取隊列元素值
result=head.next.value;
head=head.next;
//獲取元素出隊前隊列容量,並在獲取後將出隊前隊列容量減少1
if(size.getAndDecrement()==capacity){
//如果出隊前隊列容量等於隊列限額,則說明有入隊線程正在等待入隊條件
//notfull發生,因此要將相關標誌置爲true
weakenq=true;
}
}finally{
//出隊者釋放出隊鎖
deqlock.unlock();
}
//判斷入隊等待標誌
if(weakenq){
//出隊線程獲取入隊鎖
enqlock.lock();
try{
//觸發入隊條件,即隊列“不滿”條件,使等待入隊的線程能夠繼續執行
notfull.signalAll();
}finally{
//出隊線程釋放入隊鎖
enqlock.unlock();
}
}
return result;
}
}
分別使用兩個不同的鎖,enqlock和deqlock,來保證在一個時刻最多隻有一個入隊者和一個出隊者可以操作隊列對象域。這種採用兩個而不是一個鎖的方式能夠保證入隊者不會鎖住出隊者,反之亦然。每個鎖都有一個與之相關的條件變量。enqlock與notfull條件相關,即入隊需要等待隊列不滿的條件;deqlock與notempty條件相關,即出隊需要等待隊列不空的條件。同時由於隊列是有界的,所以必須跟蹤空槽的個數。size域是用來記錄隊列中併發對象個數的整型原子變量,並且入隊方法enq()會增加該值,出隊方法deq()會減少該值。
一旦空槽數大於0,入隊者就會繼續執行。注意一旦入隊者發現有空槽,則當入隊者還在繼續執行時,其他線程不能向隊列中插入元素,因爲其他的入隊者被鎖定,只會有一個併發出隊者可以增加空槽數目。
當出隊者將隊列從不滿變成滿時,他獲得enqlock並對notfull發出信號,即使size域沒有被enqlock保護,也由於出隊者在觸發條件之前已經先獲得了enqlock,所以出隊者不可能在入隊者的兩個操作步驟之間產生信號。
這種實現的一個缺點就是併發的enq()方法和deq()方法會相互干擾,但又不通過鎖。所有的方法對size域調用getAndIncrement()和getAndDecrement(),這些方法可能會產生比通常的讀-寫開銷更大,且可能引起順序瓶頸。減少這種干擾的一種方法,就是將這個域分成兩個計數器,一個由deq()減1的整型域enqsize和一個由enq()加1的整型域deqsize。調用enq()的線程檢測enqsize,只要他小於隊列容量限額,就繼續執行。當該域達到隊列容量限額時,線程鎖住deqlock並將deqsize加到enqsize中。當入隊者的大小估值變得非常大時,這種技術能夠分散地同步,而不是對每個方法調用進行同步。