第十四章 构建自定义的同步工具
一 条件队列
它使得一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变成真。
正如每个java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且Object中的wait,notify和notifyAll方法就构成了内部条件队列的API。
wait:wait是等待的意思,调用wait会自动释放锁,并请求系统挂起当前线程,从而使其他线程能够获得这个锁
notify:发出通知,解除阻塞条件,JVM会从这个条件队列上等待的多个线程选择一个来唤醒
notifyAll:发出通知,解除阻塞条件,JVM会唤醒所有在这个条件队列上等待的线程
条件谓语:线程等待的条件
条件等待中存在一个很重要的三元关系:synchronized,wait和一个条件谓语。
条件变量由一个锁保护,检查条件谓语时必须先持有锁,调用wait和notifyAll所在方法的对象必须是同一个对象。
大多数情况下,应该优先选择notifyAll。假如线程A在条件队列上等待条件谓语PA,线程B在同一个条件队列上等待条件谓语PB,假如线程C将PB变为真,且调用notify,JVM将从众多的等待线程选择其中A来唤醒,但是A看到PA仍然为false,于是继续等待,然而线程B本可以开始执行,却没有被唤醒。
只有满足一下两个条件时,才能用单一的notify而不是notifyAll:
1. 所有等待线程的类型都相同
2. 单进单出:在条件变量上的每次通知,最多只能唤醒一个线程来执行
如果有10个线程在条件队列中等待, 调用notifyAll会唤醒每一个线程, 让它们去竞争锁, 然后它们中的大多数或者全部又回到休眠状态, 这意味着每一个激活单一线程执行的事件, 都会带来大量的上下文切换, 和大量竞争锁的请求
二 显式的Condition对象
内置的条件队列有一些缺陷,每一个内置锁都只能由一个相关联的条件队列。如果想要编写一个带有多个条件谓语的并发对象,可以使用Lock和Condition。
一个Condition和一个单独的Lock相关联, 调用Lock.newCondition()方法, 可以创建一个Condition。每个Lock可以有任意数量的Condition对象. wait, notify, notifyAll在Condition中都有对应的:await, signal, signalAll, 而且一定要使用后者!
使用显式条件变量的有界缓存:
public class ConditionBoundedBuffer<T> {
private static final int BUFFER_SIZE = 2;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final T[] items = (T[]) new Object[BUFFER_SIZE];
private int tail, head, count;
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); //只要队列是满的,那么notFull等待
}
items[tail] = x;
if (++tail == items.length) {
tail = 0;
}
count++;
notEmpty.signal(); //队列不为空,那么唤醒notEmpty
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();// 只要队列是空的,那么notEmpty等待
}
T x = items[head];
items[head] = null;
if (++head == items.length) {
head = 0;
}
count--;
notFull.signal(); //队列不满,那么唤醒notEmpty
return x;
} finally {
lock.unlock();
}
}
}
三 AbstractQueuedSynchronizer(AQS)
AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS构造出来,比如ReentrantLock,Semaphore,CountDownLatch等。
AQS构建的容器中,最基本的就是获取操作和释放操作,对于CountDownLatch,获取意味着等待并直到闭锁到达结束状态,对于FutureTask,获取意味着等待直到任务已经完成。
AQS负责同步容器类中的状态,它管理了一个整数状态信息,可以通过getState,setState以及compareAndSetState来设置和获取。例如ReentrantLock用它来表示线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态(尚未开始,正在运行,已完成以及以取消)。
下面给出一个使用AQS实现的二元闭锁:
/**
* 使用AQS实现的二元闭锁
* @author cream
*
*/
public class OneShotLatch{
private final Sync sync = new Sync();
public void signal(){
sync.releaseShared(0);
}
public void await() throws InterruptedException{
sync.acquireSharedInterruptibly(0);
}
private class Sync extends AbstractQueuedSynchronizer{
protected int tryAcquireShared(int ignored){
//如果闭锁是开的(state==1),那么这个操作成功,否则失败
return (getState()==1) ? 1 : -1;
}
protected boolean tryReleaseShared(int ignored){
setState(1);//打开闭锁
return true;//表示其他线程可以获取该闭锁
}
}
}
- public final boolean releaseShared(int arg)
Releases in shared mode. Implemented by unblocking one or more threads if tryReleaseShared(int) returns true.
Parameters:arg - the release argument. This value is conveyed to tryReleaseShared(int) but is otherwise uninterpreted and can represent anything you like.
Returns:the value returned from tryReleaseShared(int)
AQS状态用来表示闭锁状态:关闭(0)或者打开(1)。signal方法会调用releaseShared,接下来又会调用tryReleaseShared,来无条件的把闭锁状态设置为打开。await方法原理类似。