- UID
- 1029342
- 性别
- 男
|
乒乓缓冲机制在很多场合都有应用价值,将其抽象成某种通用化类库,使代码得以复用。那么首先就要抽象出此机制的抽象模型。
乒乓缓冲应该有两个相同的对象作为缓冲区(对象类型可以是任意的),两者交替地被读和被写。在卷轴的例子中,向可见区域移动就是读操作,生成并绘制就是写操作。读写的过程在两个缓冲区之间交替进行:一开始两个缓冲内容均无效,不能被读;然后写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()这两个接口之外,还有一种更方便的办法。下文就介绍这个增强型的设计。 |
|