Java高级:条件队列与同步器Synchronizer的原理+AQS的应用

14.构建自定义的同步工具

类库中包含了许多存在状态依赖性的类,例如 FutureTask , Semaphore和 BlockingQueue等。在这些类中的一些操作中有着基于状态的前提条件,例如,不能从一个空的队列中删除元素,或者获取一个尚未结束的任务的计算结果,在这些操作可以执行之前,必须等到队列进入 “非空 ”状态,或者任务进入 “已完成 ”状态。

创建状态依赖类的最简单方法通常是在类库中现有的状态依赖类的基础上进行构造。本章将介绍实现状态依赖性的各种选择,以及在使用平台提供的状态依赖性机制时需要遵守的各项规则。

14.1 状态依赖性的管理

1.1 状态依赖的管理

对单线程的程序而言 ,如果基于状态的前提条件未得到满足,那么这个条件将永 远无法成真,此时失败即可。

但是在并发程序中 ,基于状态的条件可能会由于其他线程的操作而改变:一个资源池在前几条指令之前还是空的 ,但现在却变为非空的 ,因为另外一个线程往里面添加了元素。对于并发对象 ,依赖于状态的方法,虽然有时可以在不满足前提条件的情况下选择失败 , 不过更好的选择是等待前提条件为真。

状态依赖的操作可以一直阻塞直到可以继续执行,这比使他们先失败在实现起来要更为方便且更不容易出错。内置的条件队列可以使线程一直阻塞,直到对象进入某个进程可以继续执行的状态,并且当被阻塞的线程可以执行时再唤醒他们。为了突出搞笑的条件等待机制的价值。我们先介绍如何通过轮训与休眠等方式来解决状态依赖性的问题。

程序清单 14-1 可阻塞的状态依赖操作的结构

acquire lock 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
release lock

程序清单 14-1 的这种加锁模式有些不同寻常,因为锁是在操作执行过程中被释放与重新获取的。构成前提条件的状态变量必须由对象的锁保护起来 ,这样它们能在测试前提条件的过程中保持不变。如果前提条件尚未满足 ,就必须释放锁 ,让其他线程可以修改对象的状态。否则 ,前提条件就永 远无发成真了。再次测试前提条件之前 ,必须要重新获得锁 .

接下来以有届缓存的实现为例,介绍一下采用不同方式来处理前提条件失败。在每种实现中都扩展了程序清单 14-2中的 BaseBoundedBuffer ,在该类中实现了一个基于数组的循环缓存,其中各个缓存状态变量( buf、 head、 tail、 count )均有缓存的内置锁来保护。同时还提供了同步的 doPut和 doTake方法,并在子类中通过这些方法来实现 put和 take操作,底层状态对子类隐藏。

程序清单 14-2 有届缓存实现的基类

public 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];
    }

 
    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 ;
    }
}

1.2 通过 " 轮询加休眠 " 实现拙劣的阻塞

程序清单 14-3 使用简单阻塞实现的有届缓存

public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V> {

    protected SleepyBoundedBuffer(int capacity) {
        super(capacity);
    }
 
    public void put(V v) throws InterruptedException {
        //无限尝试将v添加入集合
        while (true) {
            //获得锁
            synchronized (this) {
                //如果不空,就添加进集合,退出循环
                if (!isFull()) {
                    doPut(v);
                    return;
                }
            }

            //否则释放锁,休眠一段时间,给其他线程一些修改的机会.
            Thread.sleep(1000);
        }
    }
 
    public V take() throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isEmpty()) 
                    return doTake();
            }
            Thread.sleep(1000);
        }
    }
}

另外,除了阻塞休眠等待的方式,还可以将前提条件的失败传递给调用者,由调用者控制是否进入休眠。如果调用者不进入休眠而直接重新调用的方式成为忙等待或者自旋等待。

如果缓存的状态在很长一段时间内不会发生变化,那么使用这个方式就会消耗大量的 CPU时间。但是,在进入休眠的情况下,如果缓存的状态在刚调完 sleep后就立即发生变化,那么将不必要地休眠一段时间。因此我们必须要在这两者中做出选择:要么容忍自旋导致的 CPU始终周期浪费,要么容忍由于休眠而导致的低响应性。

1.3 条件队列

通过轮询与休眠来实现阻塞操作的过程需要付出大量的努力。如果存在某中挂起线程的方法,并且这种方法能够确保当某个条件成真时线程立即醒来,那么将极大地简化实现工作。这正是条件队列的功能。

“条件队列 ”这个名字的来源于:它使得一组线程(称之为等待线程集合)能够通过某种方式来等待特定的条件变成真。传统队列的元素是一个个数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。

正如每个 Java对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且 Object中的wait、 notify和 notifuAll方法就构成了内部条件队列的 API。对象的内置锁与其内部条件队列是相互联系的,要调用对象 X中条件队列的任何一个方法,必须持有对象 X上的锁。这是因为 “等待由状态构成的条件 ”与 “维护状态一致性 ”这两种机制必须被紧密地绑定在一起:只有能对状态进行检查时,才能在某个条件上等待,并且只有能修改状态时,才能从条件等待中释放另一个线程。

Object.wait会自动释放锁,并请求操作系统挂起当前的线程,从而使其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,他将在返回之前重新获取锁。程序清单 14-6中使用了 wait和 notifyAll实现了一个有届缓存。

程序清单 14-6 使用条件队列实现的有届缓存

public class BoundedBuffer<V> extends BaseBoundedBuffer <V>{
    // 条件谓词:not-full (!isFull)
    // 条件谓词:not-empty (!isEmpty)
    protected BoundedBuffer(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 v = doTake();
        notifyAll();
        return v;
    }
}

最终这比使用休眠的有届缓存更加简单,并且更加高效(线程醒来的次数更少),响应性也更高(当发生特定状态变化时将立即醒来)。

注意:与使用休眠的有届缓存相比,条件多列并没有改变原来的语义。他只是在多个方面进行了优化: CPU效率,上下文切换开销和响应性等。如果某个功能无法通过 “轮询 +休眠 ”来实现,那么使用条件队列也无法实现,但条件队列是的在表达和管理状态依赖时更加的简单和高效。

14.2 使用条件队列

条件队列使构建高效以及高可响应性的状态依赖类变得更容易,但同时也很容易被不正确的使用。

虽然许多规则都能确保正确地使用条件队列,但在编译器或系统平台上却并没有强制要求遵循这些规则。(这也是为什么要尽量基于 LinkedBlockingQueue、 Latch、 Semaphore 和 FutureTask 等类来构造程序的原因之一,如果能避免使用条件队列,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值