乒乓緩沖機制在很多場合都有應用價值,將其抽象成某種通用化類庫,使代碼得以復用。那么首先就要抽象出此機制的抽象模型。
乒乓緩沖應該有兩個相同的對象作為緩沖區(對象類型可以是任意的),兩者交替地被讀和被寫。在卷軸的例子中,向可見區域移動就是讀操作,生成並繪制就是寫操作。讀寫的過程在兩個緩沖區之間交替進行:一開始兩個緩沖內容均無效,不能被讀;然后寫0,完畢后0可讀,再寫1使1可讀,同時可以讀0,讀完后0變成可寫狀態;1寫畢后變成可讀狀態……由此可見對於每個緩沖區來說,可能的狀態有四種並按如下順序循環往復地轉換:可讀=>在讀=>可寫=>在寫=>可讀=>……
一個BiBuf類負責維護兩個緩沖對象(長度為2的Object類型數組,名為bufs)及記錄其狀態。狀態常量這樣設定:0=在讀, 1=可讀(寫畢), 2=可寫(讀畢), 3=在寫。 於是兩個緩沖狀態的表示用一個長度為2的byte型數組即可(當然完全可以放在一個byte變量的高4位和低4位,不過不夠清晰易懂)。但僅有這個指示某塊緩沖“可以做什么”的變量還不夠,還要有個指示“應該做什么”的變量。因為對於單一的讀或寫操作來說,可操作的緩沖對象必須是交替輪換的,比如兩個緩沖都寫滿之后,都是可讀的狀態,這時實際應讀哪一個呢?所以還應該有兩個變量bufToRead和bufToWrite,分別指示當前應該讀和應該寫的緩沖序號。
緩沖對象數組被設成私有的以阻止外部對象直接對其操作。為了讀寫,BiBuf類將有一個openBuf(char request)方法用於按request指定的讀寫請求('w'為寫,'r'為讀)打開一個緩沖對象。方法會阻塞直至“應該”被操作的對象已經“可以”進行指定的操作,然后重新設定該塊緩沖對象的狀態變量。
BiBuffer.java :
public class BiBuffer {
//……
public synchronized Object openBuf(char request) {
//必須用synchronized關鍵字,以實現對狀態變量rwstat讀寫的互斥
if (request=='w') {
//若請求的是寫操作
try {
while (rwstat[bufToWrite]!=READDONE) {
//阻塞直至bufToWrite號緩沖可寫
wait();
}
}catch(InterruptedException inte){}
rwstat[bufToWrite]=WRITING;//把狀態置為在寫
return bufs[bufToWrite];//返回該塊緩沖對象
}
else if (request=='r') {
try {
while (rwstat[bufToRead]>=READDONE) {
//阻塞直至bufToRead號緩沖可讀
wait();
}
}catch(InterruptedException inte){}
rwstat[bufToRead]=READING;//把狀態置為在讀
readingReaders++;//讀者數加1(因為可以被不止一個對象讀)
return copydata(bufs[bufToRead]);//返回該塊緩沖對象的拷貝
}
else return null;
}
//……
}
需要解釋一下copydata()方法,它的作用是返回一個緩沖對象的拷貝,因為“讀”操作與“寫”操作的本質區別是,它原則上是不能修改原對象的,所以要返回一個拷貝供讀者對象去“讀”。但其默認的做法是直接返回原緩沖對象本身,這是出於效率的考慮:
public Object copydata(Object src) {
//子類可重寫之
return src;
}
當然,方法是留給子類去繼承的,具體實現依賴於子類的設計。
為什么要單獨設計一個copydata方法而不是把緩沖對象聲明為Clonable,直接調用其clone()方法呢?這僅僅是為了不必因為緩沖對象要實現Clonable接口,還要去寫一個其原型的子類罷了(何況還應考慮到有的類可能被設置為final,不可繼承的)。
一旦一個緩沖對象被openBuf()打開后,其狀態值就被標成相應的“正在進行”狀態,這時其它的線程要去打開它的話也就只能阻塞直至其完成操作(除了正在讀時另外的讀者去打開它時,因為“讀”是可以多個線程同時進行的),於是就可以放心大膽地對其進行讀寫。而讀寫操作完成后,應調用closeBuf(Object buf)方法設置buf所引用的緩沖的狀態為“完成”。
//……
public synchronized boolean closeBuf(Object buf) {
int bufid;
if (bufs[0]==buf) bufid=0;
else if (bufs[1]==buf) bufid=1;
else return false;
try {
if (rwstat[bufid]==WRITING) {
//若該塊緩沖是被寫的
rwstat[bufid]=WRITTEN;//設狀態為可讀
bufToWrite=1-bufToWrite;//當前應寫的緩沖切換到另一個
return true;
}
else if (rwstat[bufid]==READING) {
//若該塊緩沖是被讀的
if ((--readingReaders)==0) rwstat[bufid]=READDONE;//遞減讀者數量。若減到0則置狀態為可寫
bufToRead=1-bufToRead;//當前應讀的緩沖切換到另一個
return true;
}
else return false;
}finally {
notifyAll();//喚醒所有因條件不滿足而等待的線程
}
}
//……
兩個緩沖對象的初始化操作通過方法initdata來完成。它既可以接默認設計直接在由構造方法參數傳入的對象數組上進行初始化,也可以自己生成一個新的數組,具體如何做完全交給子類去重寫,賦予這個設計以充分的靈活性。下面是該類的其余部分代碼:
public class BiBuffer {
private Object bufs[];
private byte rwstat[];
private int bufToRead, bufToWrite;
private int readingReaders;
public static final byte READING = 0;
public static final byte WRITTEN = 1;
public static final byte READDONE = 2;
public static final byte WRITING = 3;
public Object[] initdata(Object[] data) {
//子類可重寫之
return data;
}
public Object copydata(Object src) {
//子類可重寫之
return src;
}
public void reset() {
rwstat[0]=READDONE; rwstat[1]=READDONE;
readingReaders=0;
bufToWrite=0; bufToRead=0;
}
public BiBuffer(Object[] data) {
rwstat=new byte[2];
reset();
bufs=initdata(data);
}
//……
}
到此為止這個類應該算比較完善了。但還有一點不太令人滿意:openBuf(),讀寫,closeBuf(),這樣的操作步驟是要靠讀寫者自己去做的,有點麻煩,況且不能保證編寫程序的人會不會忘記在用完后closeBuf。我希望在提供openBuf()和closeBuf()這兩個接口之外,還有一種更方便的辦法。下文就介紹這個增強型的設計。