[Java并发编程实战]状态依赖性的管理之阻塞队列的实现(二)

明日复明日,明日何其多?我生待明日,万事成蹉跎。———《明日歌》
这句诗给人的启示是:世界上的许多东西都能尽力争取和失而复得,只有时间难以挽留。人的生命只有一次,时间永不回头。反复告诫人们要珍惜时间,今日的事情今日做,不要拖到明天,不要蹉跎岁月。

上一篇遗留的问题: 有没有某种方法可以达到,当条件为真时,线程立即醒过来执行呢?

答案是肯定的。

所以,来看第三种方法,代码清单如下:

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 对象。

恩,本文完结!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值