状态依赖性类
类库包含了许多存在状态依赖性的类,例如FutureTask、Semaphore、BlockingQueue等。这些类的一些操作中有着基于状态的前提条件,例如不能从一个空队列中删除元素等。
创建自己的状态依赖性类最简单的方法通常是在类库中现有状态依赖类基础上进行构造,例如: 8.17
14.1 状态依赖性的管理
程序清单 14-1 可阻塞的状态依赖操作的结构
void blockingAction() throws InterruptedException {
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
}
程序清单 14-2 有界缓存实现的基类
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
/**
* FIFO 队列形式的缓存
* [,,,,,] initial head = 0 tail = 0 count = 0
* [1,,,,,] doPut: head = 0 tail = 1 count = 1
* [1,2,,,,] doPut: head = 0 tail = 2 count = 2
* [1,2,3,,,] doPut: head = 0 tail = 3 count = 3
* [1,2,3,4,,] doPut: head = 0 tail = 4 count = 4
* [1,2,3,4,5,] doPut: head = 0 tail = 5 count = 5
* [1,2,3,4,5,6] doPut: head = 0 tail = 0 count = 6
* [,2,3,4,5,6] doTake:head = 1 tail = 0 count = 5
* [,,3,4,5,6] doTake:head = 2 tail = 0 count = 4
* [,,,4,5,6] doTake:head = 3 tail = 0 count = 3
* [,,,,5,6] doTake:head = 4 tail = 0 count = 2
* [,,,,,6] doTake:head = 5 tail = 0 count = 1
* [,,,,,] doTake:head = 0 tail = 0 count = 0
* @param
*/
//数组只是个载体,放置元素。而头和尾部是按照你放置进去的元素计算的
@ThreadSafe
public abstract class BaseBoundedBuffer {
@GuardedBy("this") private final V[] buf;
@GuardedBy("this") private int tail;
@GuardedBy("this") private int head;
@GuardedBy("this") private int count;
protected BaseBoundedBuffer(int capacity) {
this.buf = (V[]) new Object[capacity];
}
protected synchronized final void doPut(V v) {
buf[tail] = v; //往数组的已有元素后面放置新值
if (++tail == buf.length) //尾部下标➕1,供下一个新元素put时使用
//如果数组被填满了,则将tail置数组初始位置,
//因为数组可能被从头部take走了几个元素,所以头部位置是有可能是空的
//此时再加入新的元素都将从数组的头部开始算
tail = 0;
++count;
}
protected synchronized final V doTake() {
V v = buf[head]; //每次取数组最前面的元素
buf[head] = null; //将数组刚刚取出的数组最前面的坑位置为null
if (++head == buf.length) //将缓存的head标志往后挪一个
//如果最后一个元素也取出了,那么++head就变成了数组的长度,
//所有元素都取光了,重新将head归位到数组的头部
head = 0;
--count;
return v;
}
public synchronized final boolean isFull() {
return count == buf.length;
}
public synchronized final boolean isEmpty() {
return count == 0;
}
@Override
public synchronized String toString() {
String result = "[";
if(buf != null && buf.length > 0)
for(V v : buf)
if(v != null)
result += (v.toString() + " | ");
else
result += (" | ");
result = result.substring(0, result.length()-2);
result += "]";
return result;
}
}
14.1.1 将前提条件的失败传递给调用者(非阻塞)
程序清单14-3 当不满足状态依赖性前提条件时,有界缓存不会执行相应的操作
队列本身不阻塞,直接抛异常,交由调用者处理。单线程可用
import net.jcip.annotations.ThreadSafe;
@ThreadSafe
public class GrumpyBoundedBuffer extends BaseBoundedBuffer {
public GrumpyBoundedBuffer(int size) { super(size); }
public synchronized void put(V v) throws Exception {
if (isFull())
throw new Exception("Buff is full");
doPut(v);
}
public synchronized V take() throws Exception {
if (isEmpty())
throw new Exception("Buff is empty");
return doTake();
}
}
程序清单14-4 GrumpyBoundedBuffer的调用
/*获取缓存中的一个元素并处理,[若没有元素则休眠一小会,重新获取],
不断循环此过程(自旋等待),直到获取到元素为止,
然后处理,然后break退出循环。
*/
public class TestCache {
//在客户端阻塞式获取缓存中的元素
public static void dealInConsumer(GrumpyBoundedBuffer buffer) {
while (true) {
try {
String item = buffer.take(); //如果没有元素则走到下面的catch
// take到了后,继续按客户端逻辑的需求进行处理
System.out.println(item);
break; //处理完了再退出轮询
} catch (Exception e) {
System.out.println("没有内容 >");
try {
Thread.sleep(2000);//休眠一段时间后继续while执行take()
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
GrumpyBoundedBuffer buffer = new GrumpyBoundedBuffer<>(2);
for (int i = 0; i < 2; i++) {
buffer.put("String " + i);
}
for (int i = 0; i < 3; i++) {
dealInConsumer(buffer);
}
}
}
忙(自旋)等待:
如果调用者在catch中不进入休眠状态,而是直接重新调用take(),若缓存状态在很长一段时间都不变化(一直没元素),将无限次地调用本判断逻辑,这将大量消耗CPU时间。
进入休眠状态:
避免消耗更多的CPU,但是若缓存状态在刚调用完sleep()就立即发送变化,那么将不必要地休眠一段时间。
14.1.2 示例:通过轮询与休眠实现简单的阻塞
调用者无需处理失败和重试,皆在实现类中处理了。
多线程中可用。在并发程序中,基于状态的条件可能因为其他线程的操作而改变。所以我们可以在条件不符合的时候等待前提条件变为真
程序清单14 - 5 使用简单阻塞实现的有界缓存
@ThreadSafe
public class SleepyBoundedBuffer extends BaseBoundedBuffer {
public SleepyBoundedBuffer(int size) { super(size); }
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
//可能条件已经被其它线程修改为true了,但是依然在sleep,不够好
Thread.sleep(SLEEP_GRANULARITY);
}
}
public V take() throws InterruptedException {
while (true) {
synchronized (this) {
if (!isEmpty())
return doTake();
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
}
14.1.3 条件队列
条件队列就好像烤面包机中的通知已经烤好了的铃声。队列中的元素的一个个正在等待相关条件的线程。Object的wait(),notify(),notifyAll()构成了内部条件队列的API。Object.wait():自动释放锁,并请求OS挂起当前线程,从而使其它线程能够获得锁并修改对象的状态。使用wait():我要去休息了,发生特定的事情时再叫醒我。而调用通知方法即表示特定的事情发生了。
如果某个功能无法通过“轮询和休眠”来实现,那么使用条件队列也无法实现
package com.multithread.unit14;
import net.jcip.annotations.ThreadSafe;
@ThreadSafe
public class ConditionBoundedBuffer extends BaseBoundedBuffer {
protected ConditionBoundedBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws InterruptedException {
if(isFull())
wait();
System.out.println("before put : " + this);
doPut(v);
System.out.println("after put : " + this);
notifyAll();
}
public synchronized V take() throws InterruptedException {
if(isEmpty())
wait();
System.out.println("before take : " + this);
V v = doTake();
System.out.println("after take : " + this);
notifyAll();
return v;
}
@Override
public String toString() {
return super.toString();
}
}
客户端调用
public static void testWatiNotify() throws InterruptedException {
final ConditionBoundedBuffer bf = new ConditionBoundedBuffer<>(3);
//用线程池执行一个put线程
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 6; i++) {
try {
Thread.sleep(1000);
String val = "v" + i;
bf.put(val);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
pool.execute(new Runnable() {
@Override
public void run() {
try {
//主线程从中获取
for (int i = 0; i < 3; i++) {
Thread.sleep(3000);
System.out.println("take out : " + bf.take());
}
Thread.sleep(3000);
System.out.println("finally buf is : " + bf);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
pool.shutdown();
}
控制台打印输出 :
before put : [ | | ]
after put : [v0 | | ]
before put : [v0 | | ]
after put : [v0 | v1 | ]
before take : [v0 | v1 | ]
after take : [ | v1 | ]
take out : v0
before put : [ | v1 | ]
after put : [ | v1 | v2 ]
before put : [ | v1 | v2 ]
after put : [v3 | v1 | v2 ]
before take : [v3 | v1 | v2 ]
after take : [v3 | | v2 ]
take out : v1
before put : [v3 | | v2 ]
after put : [v3 | v4 | v2 ]
before take : [v3 | v4 | v2 ]
after take : [v3 | v4 | ]
take out : v2
before put : [v3 | v4 | ]
after put : [v3 | v4 | v5 ]
finally buf is : [v3 | v4 | v5 ]
程序正常运行结束,因为put的元素减去take的元素刚刚好等于缓存的长度。若是大于,则程序无法退出,一直在阻塞,知道有线程从缓存中拿走了元素,并notify它。反之put少,take多亦是如此。
14.2 使用条件队列
条件队列使得构建高响应性的状态依赖类变得容易,但是同时也容易被不正确地使用。尽量使用基于LinkedBlockingQueue,Latch,Semaphore和FutureTask等类来构建程序,会更容易更简单些。
14.2.1 条件谓词
条件谓词是使某个操作成为状态依赖操作的的前提条件。是由类中各个状态变量构成的表达式。
take()条件谓词:缓存不为空
put()条件谓词:缓存未满