明日复明日,明日何其多?我生待明日,万事成蹉跎。———《明日歌》
这句诗给人的启示是:世界上的许多东西都能尽力争取和失而复得,只有时间难以挽留。人的生命只有一次,时间永不回头。反复告诫人们要珍惜时间,今日的事情今日做,不要拖到明天,不要蹉跎岁月。
上一篇遗留的问题: 有没有某种方法可以达到,当条件为真时,线程立即醒过来执行呢?
答案是肯定的。
所以,来看第三种方法,代码清单如下:
public class BoundedBuffer<V> extends BaseBoundedBuffer{
protected BoundedBuffer(int capacity) {
super(capacity);
// TODO Auto-generated constructor stub
}
public synchronized void put(V v) throws InterruptedException {
while(isFull())
wait();
doPut(v);
notifyAll();
}
public synchronized V take() throws InterruptedException {
while(isEmpty())
wait();
V v = (V) doTake();
notifyAll();
return v;
}
}
这里采用了Object的wait,notify,notifyAll方法,实现我们想要的效果。Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使得其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将在返回之前重新获取锁。
到这里,BoundedBuffer 已经变得足够好,简单易用。
每一次wait调用之前都必须持有锁,并且这个锁必须保护着构成条件谓词的状态变量。
使用 wait 常见的几个问题说明。
过早唤醒
wait 方法的返回并不一定意味着条件就为真了。比如下面几个原因
- 因为可能在发出通知的线程调用 notifyAll 时,条件谓词可能已经变成真,但在重新获取锁时再次变为假了。在线程被唤醒到wait重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。
- 条件谓词从调用wait起,根本就没有变成真。因为你并不知道另一个线程为什么调用 notify 和 notifyAll,也许可能是另一个条件谓词变为真了。
基于上面两个原因,每当线程从 wait 中唤醒时,都必须再次测试条件谓词。如果不为真,就继续等待。由于可能每次醒来条件谓词都不为真,所以必须在一个循环中调用 wait,每次循环都要判断条件谓词。
void stateDependentMethod() throws InterruptedException {
synchronized(lock) {
while(!conditionPredicate()) {//此处必须是循环
lock.wait();
}
}
}
丢失的信号
丢失的信号是指:线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词。现在,线程将等待一个已经发过通知的事件。导致一直处于等待状态。
通知 notify 和 notifyAll
两者的区别是,notify 只能唤醒一个线程,也就是说在众多等待的线程中随机挑选一个唤醒。notifyAll是唤醒所有的等待线程。假如多个线程基于不同的条件谓词在同一个条件队列上等待,使用notify将是危险的,因为它只唤醒一个并且不保证唤醒的就是与之相对应的条件谓词。
所以优先使用 notifyAll 而不是 notify。 然而,这样还是有可能导致性能上的问题。
假如有 10 个线程在等待,那么调用 notifyAll 将唤醒所有线程,并使得他们在锁上发生竞争。然后大多数又回到睡眠状态。因此,在每个线程执行一个事件的同时,将出现大量的上下文切换操作以及发生竞争的锁获取操作。
上面说的第三种方式,称为使用内置条件队列,因为它使用的是内置锁 synchronized。它存在一些缺陷,每个内置锁都只能有一个相关联的条件队列,而像BoundedBuffer这种类中,多个线程可能在同一个条件队列上等待不同的条件谓词,这使得 notifyAll 唤醒的不是同一类型的条件谓词。
如果想要更加细分,唤醒不同的条件谓词,可以使用 Lock 和 Condition,代替内置锁,它是一种更加灵活的选择。
Condition 接口如下:
public interface Condition {
void await() throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUninterrupteibly();
boolean awaitUnit(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
对于每个 Lock, 可以有任意数量的 Condition 对象。在Condition对象中,与wait,notify,notifyAll方法对应的是 await,signal,signalAll.
所以,实现缓存有界队列的第四种方法是使用两个 Condition, 分别为 notFull 和 notEmpty。如下代码所示:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionBoundedBuffer<T> {
protected final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final T[] items = (T[]) new Object[10];
private int tail, head, count;
public void put(T t) throws InterruptedException{
lock.lock();
try {
while(count == items.length)
notFull.await();
items[tail] = t;
if(++tail == items.length)
tail = 0;
++count;
notEmpty.signal();
}finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while(count == 0)
notEmpty.await();
T t = items[head];
if(++head == items.length)
head = 0;
--count;
notFull.signal();
return t;
}finally {
lock.unlock();
}
}
}
没错,这就是最终版本了,类库中的 ArrayBlockingQueue 就是这样实现的。ConditionBoundedBuffer 的行为和 BoundedBuffer 相同,但它对条件队列的使用方式更加容易理解。多个Condition使得我们分析代码时更加清晰,并且使用signal极大的减少在每次缓存操作中发生的上下文切换和锁请求的次数。
与内置锁和条件队列一样,当使用显示的 Lock 和 Condition 时,也必须满足锁,条件谓词和条件变量之间的三元关系。在条件谓词中包含的变量必须由 Lock 来保护,并且在检查条件谓词以及调用 await 和 signal 时,必须持有 Lock 对象。
恩,本文完结!