类库中包含了很多状态依赖性的类,例如FutureTask、Semaphore和BlockingQueue等。在这些类的一些操作中有着基于状态的前提条件,例如,不能从一个空的队列中删除元素,或者获取一个尚未结束的任务的计算结果,在这些操作可以执行之前,必须等到队列进入“非空”状态,或者任务进入“已完成”状态。
在单线程程序中调用一个方法时,如果某个基于状态的前提条件未得到满足(例如“连接池必须非空”),那么这个条件将永远无法成真。因此,在编写顺序程序中的类时,要使得这些类在他们的前提条件未满足时就失败。但在并发程序中,基于状态的条件可能会由于其他线程的操作而改变:一个资源池可能在几条指令之前还是空的,但现在却变为非空的,因为另一个线程可能会返回一个元素到资源池。对于并发对象上依赖状态的方法,虽然有时候在前提条件不满足的情况下不会失败,但通常有一种更好的选择,即等待前提条件为真。
依赖状态的操作可以一直阻塞直到可以继续执行,这比使他们先失败再实现起来要更为方便且不容易出错。内置的条件队列可以使线程一直阻塞,直到对象进入某个进程可以继续执行的状态,并且当被阻塞的线程可以执行时再唤醒它们。
可阻塞的状态依赖操作的形式如下,这种加锁模式有些不太寻常,因为锁是在操作的执行过程中被释放与重新获取的。构成前提条件的状态变量必须由对象的锁来保护,从而使它们在测试前提条件的同时保持不变。如果前提条件尚未满足,就必须释放锁,以便其它线程可以修改对象的状态,否则,前提条件就永远无法变成真。在再次测试前提条件之前必须重新获得锁。
1.可阻塞的状态依赖操作的结构
acquire lock on object state
while(precondition does not hold){
release lock
wait until precondition might hold
optionally fail if interrupted or timeout expires
reacquire lock
}
perform action
release lock
在生产者-消费者的设计中经常会使用像ArrayBlockingQueue这样的有界缓存。接下来介绍有界缓存的几种实现。
2.有界缓存实现的基类
import org.apache.http.annotation.GuardedBy;
import org.apache.http.annotation.ThreadSafe;
/**
* 有界缓存实现的基类
*/
@ThreadSafe
public abstract class BaseBoundedBuffer<V> {
@GuardedBy("this") private final V[] buf;
@GuardedBy("this") private int tail;
@GuardedBy("this") private int head;
@GuardedBy("this") private int count;
@SuppressWarnings("unchecked")
protected BaseBoundedBuffer(int capacity){
this.buf = (V[])new Object[capacity];
}
protected synchronized final void doPut(V v){//往缓存中放入元素
buf[tail] = v;
if(++tail == buf.length){
tail = 0;
}
++count;
}
protected synchronized final V doTake(){//从缓存中取出元素
V v = buf[head];
buf[head] = null;
if(++head == buf.length){
head = 0;
}
--count;
return v;
}
public synchronized final boolean isFull(){//判断缓存数组中元素是否填满
return count == buf.length;
}
public synchronized final boolean isEmpty(){//判断缓存数组中元素是否为空
return count == 0;
}
}
3.将前提条件的失败传递给调用者
下面是一个简单的有界缓存实现,put和take方法都进行了同步以确保对缓存状态的独占访问,因为这两个方法在访问缓存时都采用“先检查,再运行”的逻辑策略。
当不满足前提条件时,有界缓存不会执行相应的操作。
简单的有界缓存实现
@ThreadSafe
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V>{
protected GrumpyBoundedBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws BufferFullException{
if(isFull()){
throw new BufferFullException();
}
doPut(v);
}
public synchronized V take() throws BufferNullException{
if(isEmpty()){
throw new BufferNullException();
}
doTake();
}
}
4.调用GrumpyBoundedBuffer的代码
while(true){
try{
V item = buffer.take();
//对item执行一些操作
break;
}catch(BufferEmptyException e){ Thread.sleep(SlEEP_GRANULARITY);
}}
5.通过轮询与休眠来实现简单的阻塞
使用简单阻塞实现的有界缓存
@ThreadSafe
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V>{
protected SleepyBoundedBuffer(int capacity) {
super(capacity);
}
public void put(V v) throws InterruptedException{
while(true){
synchronized(this){
if(!isFull()){
doPut(v);
return;
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
public V take() throws InterruptedException{
while(true){
synchronized(this){
if(!isEmpty()){
return doTake();
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
}
6.条件队列
“条件队列”这个名字来源于:它使一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变为真。传统队列的元素是一个个数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。
正如每个Java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且Object中的wait、notity和notifyAll方法就构成了内部条件队列的API。对象的内置锁与其内部条件队列是相互关联的,要调用对象X中条件队列的任何一个方法,必须持有对象X上的锁。这是因为“等待由状态构成的条件”与“维护状态一致性”这两种机制必须被紧密地绑定到一起:只有能对状态进行检查时,才能在某个条件上等待,并且只有能修改状态时,才能从条件等待中释放另一个线程。
Object.wail会自动释放锁,并请求操作系统挂起当前线程,从而使其它线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将会在返回之前重新获取锁。从直观上来理解,调用wait意味着“我要去休息了,但当发生特定的事情时唤醒我”,而调用通知方法就意味着“特定的事情发生了”。
下类用了wait和notifyAll来实现一个有界缓存。这比使用“休眠”的有界缓存更简单,并且更高效(当缓存状态没有发生变化时,线程醒来的次数将更少),响应性也更高(当发生特定状态变化时将立即醒来)。这是一个较大的改进,但要注意:与使用“休眠”的有界缓存相比,条件队列并没有改变原来的语义。它只是在多个方面进行了优化:CPU效率、上下文切换开销和响应性等。如果某个功能无法通过“轮询和休眠”来实现,那么使用条件队列也无法实现,但条件队列使得在表达和管理状态依赖性时更加简单和高效。
import org.apache.http.annotation.ThreadSafe;
/**
* 使用条件队列实现的有界循环
* @Class_Name: BoundedBuffer
*/
@ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
//条件谓词:not-full(!isFull())
//条件谓词:not-empty(!isEmpty())
protected BoundedBuffer(int capacity) {
super(capacity);
}
//阻塞并直到:not-null
public synchronized void put(V v) throws InterruptedException{
while(isFull()){
wait();
}
doPut(v);
notifyAll();
}
//阻塞并直到:not-empty
public synchronized V take() throws InterruptedException{
while(isEmpty()){
wait();
}
V v = doTake();
notifyAll();
return v;
}
}