Java文档上的例子,利用Condition来实现阻塞队列。
假设有一个固定大小的缓冲区(缓冲区是队列,遵守FIFO),支持存和取方法。如果缓冲区为空时尝试取数据,那么线程将阻塞,直到缓冲区有一个可用的数据;如果缓冲区满了,这时尝试写数据,那么线程将被阻塞直到有可用空间。我们希望有两个独立的等待集(Condition阻塞队列),一个放置存数据的等待线程,一个放置取数据的等待线程,这样,当缓冲区有空位置的时侯,可以到“存数据的线程等待集”中唤醒一个线程存数据;当缓冲区从一无所有到有数据存入时,可以到“取数据的线程等待集”中唤醒一个线程取数据。这个关系不能搞错!
Condition是由Lock创建的,每个Lock锁可以创建多个Condition对象。只有同一把锁的Condition对象可以相互影响
public class ConditionDemo {
static class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();// 存数据的线程等待集——因为满了而等待
final Condition notEmpty = lock.newCondition();// 取数据的线程等待集——因为空了而等待
/* 用数组实现缓冲队列(FIFO) */
final Object[] items = new Object[5];
/*
* putptr:存数据的索引位置
* takptr:取数据的索引位置
* count:当前队列大小
*/
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();/* 给需要同步的代码加锁 */
try {
/*如果数组满了,当前线程进入存数据的等待集。不用if是怕线程假醒*/
while (count == items.length) {
System.err.println(Thread.currentThread().getName()+"写入数据["+putptr+"]异常:缓冲池已满");
notFull.await();
}
/* 数据写入 */
System.out.println(Thread.currentThread().getName()+"写入数据["+putptr+"]:"+String.valueOf(x));
items[putptr] = x;
/* 改变写入索引位置+1(为下次写入数据准备),如果超出数组边界,则重置为数组起始点 */
if (++putptr == items.length)
putptr = 0;
/* 增加数组有效果元素计数,即:队列大小加1 */
++count;
/* 至此队列中肯定有可用数据了,从“取数据的线程等待集”中唤醒线程 */
notEmpty.signal();
} finally {
/* lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放 */
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();/* 给需要同步的代码加锁 */
try {
/*如果数组为空,当前线程进入读数据的等待集。不用if是怕线程假醒*/
System.err.println("线程预备");
while (count == 0){
System.err.println(Thread.currentThread().getName()+"读取数据["+putptr+"]异常:缓冲池为空");
notEmpty.await();
}
System.err.println("线程开始读数据");
/*读取数据*/
Object x = items[takeptr];
System.out.println(Thread.currentThread().getName()+"获取队列数据["+takeptr+"]:"+x);
/* 改变读取索引位置+1(为下次读数据准备),如果超出数组边界,则重置为数组起始点 */
if (++takeptr == items.length)
takeptr = 0;
/* 减少数组有效果元素计数,即:队列大小减1 */
--count;
/* 至此队列中肯定有可用空间了,从“存数据的线程等待集”中唤醒线程 */
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
final BoundedBuffer boundbuffe =new BoundedBuffer();
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(3000));
int temp=new Random().nextInt(1000);
boundbuffe.put(temp);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
for(int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(500));
boundbuffe.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
例子不难理解,我用中文注释了一下。调整存、取线程的开启数量和sleep时间,可以轻松制造“写数据异常”
运行产生的结果(部分截图):
想到一个问题,condition的await()方法让一个线程等待后,等到这个线程被唤醒时,它是接着往下执行呢?还是refresh,重新执行整个方法或lock块呢?
为了求证,在take()方法内用两个输出包围notEmpty.await()代码块,变成:
/*如果数组为空,当前线程进入读数据的等待集不用if是怕线程假醒*/
System.out.println(Thread.currentThread().getName()+"线程预备");
while (count == 0){
System.err.println(Thread.currentThread().getName()+"读取数据["+putptr+"]异常:缓冲池为空");
notEmpty.await();
}
System.out.println(Thread.currentThread().getName()+"线程开始读数据");
重新运行结果:
“线程预备”这句话只出现了一次,表明如果线程await()后被其它线程重新唤醒后,是继续往下运行。