目录
思维导图
1 管理状态依赖性
在顺序结构中,如果类的先验条件无法满足,就会标为失败。但是在并发程序中,会被其它线程活动所改变。
1.1 示例:将先验条件失败传给调用者
如demo-1是一个有限缓存base类实现。
public abstract class BaseBoundedBuffer<V> {
private final V[] buffer;
private int head;
private int tail;
private int count;
protected BaseBoundedBuffer(int capacity) {
buffer = (V[]) new Object[capacity];
}
protected synchronized void doPut(V v) {
buffer[tail] = v;
if (++tail == buffer.length) {
tail = 0;
}
count++;
}
protected synchronized V doTake() {
V result = buffer[head];
if (++head == buffer.length) {
head = 0;
}
count--;
return result;
}
public synchronized boolean isEmpty() {
return count == 0;
}
public synchronized boolean isFull() {
return count == buffer.length;
}
}
检查再运行,故方法均需要同步。
demo-2是一个base类的实现:
/**
* 检查再运行
* @param <V> 类型
*/
public class GrumpyBoundBuffer<V> extends BaseBoundedBuffer<V>{
protected GrumpyBoundBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws BufferFullException {
if (isFull()) {
throw new BufferFullException();
}
doPut(v);
}
public synchronized V take() throws BufferEmptyException {
if (isEmpty()) {
throw new BufferEmptyException();
}
return doTake();
}
//调用GurmpyBoundBuffer的客户端逻辑
public void client() throws InterruptedException {
GrumpyBoundBuffer<String> stringGrumpyBoundBuffer = new GrumpyBoundBuffer<>(100);
while (true) {
try {
String take = stringGrumpyBoundBuffer.take();
//处理take
} catch (BufferEmptyException e) {
//空队列,休眠在尝试
Thread.sleep(200);
//或者直接重试,不用忙等待
//或者取两者折中使用yield,放弃cpu重新进入就绪队列
Thread.yield();
}
}
}
}
这里先验条件失败:缓存满或者缓存空,都将会视为失败,抛给调用者异常。我们有两种方式处理异常:
- 忙等待:调用者直接进行重试,不停自旋,该方法消耗cpu,适用短期可以成功。
- 采取休眠措施,防止消耗cpu,但是可能错过依赖条件改变。
1.2 轮询+休眠实现阻塞
如demo-3实现了封装轮询+休眠重试机制:
public class SleepyBound<V> extends BaseBoundedBuffer<V> {
protected SleepyBound(int capacity) {
super(capacity);
}
//轮旋+休眠
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(500);
}
}
//轮旋+休眠
public V take() throws InterruptedException {
while (true) {
synchronized (this) {
if (!isEmpty()) {
return doTake();
}
}
Thread.sleep(500);
}
}
}
该方法相比demo-2有所改善,调用者不必处理失败和重试,这些已经有缓存实现了。
这里我们需要再响应性和cpu使用率做出权衡:休眠间隔越小,响应性越好,单cpu消耗越高。
1.3 让条件队列来解决这一切
条件队列可以让一组线程(等待集)——以某种方式等待相关条件变为真。
java每个对象都可以作为锁,也可以作为条件队列,Object的wait、notify和notifyAll就是内部队列的API。
- wait:释放锁,并请求OS挂起。
- notify:通知调用notify的对象的其中一个挂起线程进行唤醒。
- notifyAll:通知所有等待线程进行唤醒。
下列demo-4利用了wait和notify实现有限队列:
/**
* 通过wait notifyAll阻塞和唤醒对象上的线程
* @param <V>
*/
public class BoundBuffer<V> extends BaseBoundedBuffer<V> {
protected BoundBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws InterruptedException {
//阻塞唤醒后必须再次测试条件谓词
while (isFull()) {
wait();
}
doPut(v);
notifyAll();
}
public synchronized V take() throws InterruptedException {
//阻塞唤醒后必须再次测试条件谓词
while (isEmpty()) {
wait();
}
V result = doTake();
notifyAll();
return result;
}
}
这里使用对象的条件队列,可以更高效的唤醒,只有依赖条件变化才会唤醒等待线程。
2 使用条件队列
2.1 条件谓词
条件谓词是先验条件的第一站,它在一个操作和状态之间建立了依赖关系。
将条件谓词和与之关联的条件队列,以及在条件队列种等待的操作,都写入文档。
一个重要的三元关系:条件谓词涉及状态变量,状态变量由锁保护,所以在测试条件谓词之前需要持有锁。
锁对象和条件队列对象(这里是wait和notify对象)需要是同一个对象。
2.2 过早的唤醒
一个单独的条件队列可以和多个条件谓词共同使用。
这也就意味着,当notifyAll唤醒你的线程时,不一定你需要的条件谓词已经满足。
因此当wait唤醒时,都必须再次测试谓词条件。
一个等待条件谓词满足的规范式如下:
private final Object obj = new Object();
//条件等待的规范式
public void stateDependentMethod() throws InterruptedException {
//条件谓词必须由锁保护
synchronized (obj) {
while (!conditionPredicate()) {
obj.wait();
}
//处理对象
}
}
当使用条件等待时:
- 永远设置一个条件谓词——线程执行前必须满足它。
- 永远在调用wait前测试条件谓词,在唤醒后再次判断条件谓词。
- 永远在循环中调用wait。
- 确保条件谓词的状态变量由锁保护,而这个锁与条件队列相关联。
- 当调用wait、notify和notifyAll要持有与条件队列关联的锁,并且在检查条件谓词之后,执行被保护的逻辑之前,不要释放锁。
2.3 丢失的信号
当一个线程等待的信号变为真,但是在等待前检查条件谓词却变为了假,也就是丢失信号。
2.4 通知
无论合适,当你在等待一个条件时,一定要确保有人会在条件谓词变为真时通知你。
demo-4说明了为何使用nofityAll而不是notify通知对象,因为这里涉及到两个条件谓词非空和非满的判断,如果单一唤醒一个线程,可能出现信号丢失。
只有满足下列条件,才能用单一的notify代替notifyAll:
- 相同的等待者,只有一个条件谓词与条件队列关联,每个线程从wait唤醒后执行相同的等待逻辑。
- 一个对条件队列的通知,至多只激活一个线程。
2.5 示例:阀门类
使用条件等待的阀门类,如demo-5:
public class ThreadGate {
private boolean isOpen;
private int generation;
public synchronized void close() {
isOpen = false;
}
public synchronized void open() {
isOpen = true;
generation++;
notifyAll();
}
public synchronized void await() throws InterruptedException {
int awaitGeneration = this.generation;
//防止快速打开,快速关闭,导致所有线程无法被释放。
while (!isOpen && awaitGeneration == this.generation) {
wait();
}
//做action
}
}
每次开启使用自增的generation主要是防止快速的打开关闭,导致线程无法被唤醒。
2.6 子类的安全问题
一个依赖于状态的类,要么将它的等待和通知暴露给子类,要么完全阻止子类参与。
3 显式的Condition对象
Lock是广义的内部锁,Condition是广义的内部条件队列。
Condition条件队列的接口如下:
demo-6是使用Condition来构建缓存队列的方式:
public class ConditionBoundedBuffer<V> {
protected final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final V[] buffer;
private int head, tail, count;
public ConditionBoundedBuffer(int capacity) {
buffer = (V[]) new Object[capacity];
}
public void put(V v) throws InterruptedException {
lock.lock();
try {
while (count == buffer.length) {
notFull.await();
}
buffer[tail] = v;
++count;
if (++tail == buffer.length) {
tail = 0;
}
notEmpty.signal();
} finally {
lock.unlock();
}
}
public V take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
V result = buffer[head];
--count;
if (++head == buffer.length) {
head = 0;
}
notFull.signal();
return result;
} finally {
lock.unlock();
}
}
}
这里可以看出相比wait的好处,一个锁可以由多个Condition条件队列,可以更精确的唤醒等待线程,减少相当数量的上下文转化。
4 剖析Synchronizer
我们经常使用的同步器基本都是基于AbstractQueuedSynchronizer
,我们也可以根据这些基于类,进行扩展自己的同步器,比如demo-7的一个计数计数器:
/**
* 基于锁和条件实现一个信号量
*/
public class SemaphoreOnLock {
private final Lock lock = new ReentrantLock();
private final Condition permitCondition = lock.newCondition();
private int permits;
/**加锁主要是保证permits可见性。
*
* @param permitCount 信号数
*/
public SemaphoreOnLock(int permitCount) {
lock.lock();
try {
permits = permitCount;
} finally {
lock.unlock();
}
}
public void acquire() throws InterruptedException {
lock.lock();
try {
while (permits == 0) {
permitCondition.await();
}
--permits;
} finally {
lock.unlock();
}
}
public void release() {
lock.lock();
try {
++permits;
permitCondition.signal();
} finally {
lock.unlock();
}
}
}
通过利用lock和permit确保信号量的获取和释放。
5 AbstractQueuedSynchronizer
一个基于AQS的同步器基本操作,都是一些不同形式的获取和释放。AQS维护了对状态的管理,和条件队列的维护。
AQS的获取和释放基本操作如下所示:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
其中tryAcquire和tryRelease
需要子类进行实现。而阻塞队列和唤醒线程不需要我们关心。
一个获取操作通常包括两步:
- 判断当前状态是否允许获取,允许就让线程执行。否则就阻塞线程。
- 可能进行的状态更新。
5.1 一个简单的闭锁
demo-8是一个依赖AQS实现的闭锁:
public class OneShotLatch {
private final Sync sync = new Sync();
/**
* tryReleaseShared设置状态为1,返回true,会唤醒所有等待的线程
*/
public void signal() {
sync.releaseShared(0);
}
/**
* 初始时都会阻塞,因为tryAcquireShared<0
* @throws InterruptedException
*/
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(0);
}
private class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryReleaseShared(int arg) {
setState(1);
return true;
}
@Override
protected int tryAcquireShared(int arg) {
return getState() == 1 ? 1:-1;
}
}
}
通过实现AQS的模板方法来完成对线程的阻塞和唤醒。await和signal
相当于获取和释放
6 JUC中实现AQS的同步器
6.1 ReentrantLock
只支持独占的操作。实现的tryAcquire和tryRelease
如下:
这里判断状态变量如果为0试图进行原子交换,如果成功获取锁,否则失败。进入阻塞队列等待唤醒。
这里需要判断是否当前线程有资格进行释放,之后再进行释放操作修改状态等。
6.2 Semaphore和CountDownLatch
两者实现方式类似,都是通过维护一个许可,来进行判断操作是否成功。
Semaphore的实现acquire:
通过判断获取后是否小于0进行操作,如果小于0则直接获取失败,进行阻塞,否则,尝试原子交换,成功的话,返回获取成功,失败则再次进行循环。
释放操作:
这里释放就是简单的循环原子更改状态变量。然后由AQS唤醒阻塞的线程。
参考文献
[1]. 《JAVA并发编程实战》.