JAVA开发实战状态依赖性的管理之阻塞队列的实现
类库本身包含了许多存在状态依赖性的类。如FutureTask,BlockingQueue等。这些类中的一些操作,会基于状态的前提条件。比如,不能从一个空的队列删除元素或获取一个尚未结束的任务的计算结果。这两个操作执行之前,必须等到队列进入非空状态或者任务进入已完成状态。我们创建状态依赖类最简单的方法是在类库的基础上进行构造。但是如果类库没有你想要的功能,那么还可以利用Java语言和类库提供的底层机制来构造自己的同步机制。
所以,本篇要介绍如何去构造一个自己的状态依赖类。从最简单的构造一步一步介绍到复杂的规范的构造,从而了解这个过程,知道是如何得到最后的结果。
状态依赖性的管理
可阻塞的状态依赖操作如下伪代码所示:
acquire lock on object state //首先获取锁 while (precondition does not hold) { //前提条件是否满足,不满足则一直循环重试 release lock //释放锁 wait until precondition might hold //等待知道满足前提条件 optionally fail if interrupted or timeout expire //中断或者超时,各种异常 reacquire lock //重新获取锁 } perform action //执行任务 release lock //释放锁
获取锁,检查条件是否满足,如果不满足,则释放锁进入阻塞状态,直到条件满足或者中断、超时等,重新获取锁。执行任务,释放锁。
现在看这个伪代码可能还不能够直观的理解,没事,往下看,看完这篇文章就知道他的意思了,每个操作都是这个伪代码架构构造的。
ArrayBlockingQueue是一个有界缓存,提供的两个操作,put 和 take。 它们都包含一个前提条件:不能将元素放入到已满的缓存中,不能从空缓存中获取元素。恩,我们的目标就是构造这样一个ArrayBlockingQueue。
接下来,介绍2种有界缓存的实现,它们采用不同的方法来处理前提条件不满足的情况。
首先,来看下面一个基类 BaseBoundeBuffer, 后面的实现都扩展这个基类。它是一个基于数组的循环缓存,包含的变量 buf、head、tail、count都由缓存的内置锁保护。它还提供了同步的 doPut 和 doTake 方法,并在子类中,通过这些方法来实现 put 和 take 操作,底层的状态将对子类隐藏。
public abstract class BaseBoundedBuffer<V> { private final V[] buf; private int tail; private int head; private int count; protected BaseBoundedBuffer(int capacity) { this.buf = (V[]) new Object[capacity]; count = 0; } 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; } }
第一种有界缓存的实现,对 put 和 take 方法都进行同步,先检查后执行,失败则抛出异常。
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer{ 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 BufferFullException { if(isEmpty()) throw new BufferFullException(); return (V) doTake(); } }
如上所示,对于前提条件不满足的情况,都直接抛出异常,这里所谓的异常,是指缓存满或空。实际上来讲,这异常不代表着程序出错,打个比方,看到红灯并不意味着信号灯出现了异常,而是等待直到绿灯在过马路。所以,这里的意思是要求在调用方捕获异常,并每次缓存操作时都需要重试。
我们直接来看下面的客户端调用代码:
private static GrumpyBoundedBuffer gbb = new GrumpyBoundedBuffer(5); ...while(true) { try { V item = gbb.take(); break; } catch(BufferEmptyException e) { Thread.sleep(500); } }
说白了就是在不满足前提条件的情况下,再试一次,直到条件满足,让看起来能够达到阻塞的效果。但是这种情况,调用者必须自行处理前提条件是失败的情况,并且一直占用CPU。这里的问题是调用者使用这个队列会很麻烦!
第二种方法,SleepyBoundedBuffer 通过轮询和休眠来实现简单的阻塞的重试机制,从而使得调用者剥离了重试机制,简化了对缓存的使用。请看下面的代码清单:
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer{ protected SleepyBoundedBuffer(int capacity) { super(capacity); // TODO Auto-generated constructor stub } public void put(V v) throws InterruptedException { while(true) { synchronized(this) { if(!isFull()) { doPut(v); return; } } Thread.sleep(200); } } public V take() throws InterruptedException{ while(true) { synchronized(this) { if(!isEmpty()) { return (V) doTake(); } } Thread.sleep(200); } } }
从调用者的角度看,这种方法可以很好的运行。假如某个操作满足前提条件,则立即执行,否则就阻塞。调用者无需处理失败和重试,但是调用者仍然需要处理InterruptedException。与大多数具备良好行为的阻塞库方法一样,SleepyBoundedBuffer 通过中断来支持取消。
SleepyBoundedBuffer的问题在于,睡眠时间设置多长才是合理的?如何才能达到性能的最优?如下图所示,B线程设置条件为真的,但此时A仍然在睡眠,这个睡眠就是性能的瓶颈所在了。
恩,有没有某种方法可以达到,当条件为真时,线程立即醒过来执行呢?
卖个关子,下一篇为你讲解!
以上就是JAVA开发实战状态依赖性的管理之阻塞队列的实现的详细内容,更多请关注其它相关文章!