手记

java并发编程之wait&notify VS lock&condition

2018-07-06 08:50:585391浏览

若鱼1919

2实战 · 7手记 · 1推荐

jdk5之前线程同步可以用synchronized/wait/notify来进行控制,jdk5以后新添加了lock/condition。他们之间有什么联系与区别的?本文就用一个例子循序渐进的给大家展示一下:

首先来看一个有界缓存的例子:

abstract class BaseBoundedBuffer<V> {
    private final V[] buff;
    private int tail;
    private int head;
    private int count;
    protected BaseBoundedBuffer(int capacity){
        this.buff = (V[])new Object[capacity];
    }
    protected synchronized final void doPut(V v){//存
        buff[tail] = v;
        tail++;
        if(tail == buff.length){
            tail = 0;
        }
        count++;
    }
    protected synchronized final V doTake(){//get
        V v = buff[head];
        buff[head] = null;
        head++;
        if(head == buff.length){
            head = 0;
        }
        count--;
        return v;
    }
    protected synchronized final boolean isFull(){//是否是满的
        return count == buff.length;
    }
    protected synchronized final boolean isEmpty(){//是否是空的
        return count == 0;
    }
}
class GrumpBoundedBufer<V> extends BaseBoundedBuffer<V>{
    public GrumpBoundedBufer(int size){
        super(size);
    }
    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();
    }
}

当然,上面的这种实现非常不友好,如果不满足先验条件就抛出异常,但是在多线程条件下,先验条件不会保持一个一成不变的状态,队列里面的元素是在不停的变化的,因此我们用轮询加休眠改进一下:

class SleepyBoundedBufer<V> extends BaseBoundedBuffer<V>{
    public SleepyBoundedBufer(int size){
        super(size);
    }
    public void put(V v) throws InterruptedException {
        while(true){
            synchronized(this){
                if(!isFull()){//如果不是满的,可以存
                    doPut(v);
                    return;
                }
            }
            //如果是满的,休眠1秒钟,然后重试
            Thread.sleep(1000);
        }
    }
    public V take() throws InterruptedException {
        while(true){
            synchronized(this){
                if(!isEmpty()){//如果不是空的,就可以取
                    return doTake();
                }
            }
            //如果是空的,休眠1秒钟,重试
            Thread.sleep(1000);
        }
    }
}

这种轮训+休眠的方式的缺点:
(1)休眠多少时间合适呢?
(2)给调用者提出处理InterruptedException的新的要求,因为sleep是会抛出这个异常的。

如果存在一种线程挂起的方式,它能保证,在某个条件变为真的时候,线程可以及时的苏醒过来,那就太好了!这就是条件队列所做的事情。

使用内部条件队列的实现方式:

class BoundedBufer<V> extends BaseBoundedBuffer<V>{
    protected BoundedBufer(int size) {
        super(size);
    }
    public synchronized void put(V v) throws InterruptedException {
        while(isFull()){//注意这里的while,而不是if
            wait();//如果是满的,把当前线程挂起
        }
        doPut(v);//如果不满,就可以存
        notifyAll();//存了以后,唤醒所有的等待线程,因为可能有线程在等待取,放进来以后就可以取了
    }
    public synchronized V take() throws InterruptedException {
        while(isEmpty()){//注意这里的while,而不是if
            wait();//如果是空的,把当前线程挂起
        }
        V v = doTake();//如果不空,取出来
        notifyAll();//然后唤醒所有的等待线程,因为有的线程可能在等待放,取出来以后就可以放了
        return v;
    }
}

这也是jdk5之前的解决方式。
条件队列可以让一组线程(叫做:等待集wait set)以某种方式等待相关条件变为真,条件队列的元素不同于一般的队列,一般队列的元素是数据项,条件队列的元素是线程。每个java对象都有一个内部锁,同时还有一个内部条件队列。一个对象的内部锁和内部条件队列是关联在一块的。Object.wait会自动释放锁,并请求os挂起当前线程,这样就给其他线程获得锁并修改对象状态的机会,当线程被唤醒以后,它会重新去获取锁。调用wait以后,线程就进入了对象的内部条件队列里面等待,调用notify以后,就从对象的内部条件队列里面选择一个等待线程,唤醒。 因为会有多个线程因为不同的原因在同一个条件队列中等待,因此,用notify而不用notifyAll是危险的!有的线程是在take()的时候阻塞,它等待的条件是队列不空,有的线程是在put()的时候阻塞,它等待的条件是队列非满。 如果调用了take()以后notify的是总是阻塞在take上的线程,就挂了!

BoundedBufer的put和take是一种很保守的做法,每次向队列里面添加或者移除都进行notifyAll,可以进行如下的优化:
是有从空变为了非空,或者是从满变为了不满的时候,才需要从条件队列里面唤醒一个线程。

class ConditionalBoundedBufer<V> extends BaseBoundedBuffer<V>{
    protected ConditionalBoundedBufer(int size) {
        super(size);
    }
    public synchronized void put(V v) throws InterruptedException {
        while(isFull()){
            wait();
        }
        boolean isEmpty = isEmpty();
        doPut(v);
        if(isEmpty){//从空变为了非空的时候,才需要唤醒(而实际上需要唤醒那些take线程,而不是put线程)
            notifyAll();
        }
    }
    public synchronized V take() throws InterruptedException {
        while(isEmpty()){
            wait();
        }
        boolean isFull = isFull();
        V v = doTake();
        if(isFull){//从满变为了不满,才需要唤醒(而实际上需要唤醒那些put线程,而不是take线程)
            notifyAll();
        }
        return v;
    }
}

这只是一种小技巧,会加大程序的复杂性,不提倡!
从空变为了非空,唤醒的应该是那些阻塞在take()上的,从满变为了不满唤醒的应该是那些阻塞在put()上的线程,而notifyAll会把所有条件队列里面的所有的等待的线程全部唤醒,这就显现出了内部条件队列有一个缺陷:内部锁只能有一个与之关联的条件队列。显式的condition的出现就是为了解决这个问题。
正如Lock提供了比内部锁更丰富的特征一样,condition也提供了比内部条件队列更丰富更灵活的功能。一个lock可以有多个condition,一个condition只关联到一个Lock。

class ConditionBoundedBufer<T> {//使用显式的条件变量,HLL的登场了
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
    private final T[] items = (T[])new Object[100];
    private int head,tail,count;
    
    //阻塞,一直到notFull
    public void put(T t) throws InterruptedException {
        lock.lock();
        try{
            while(count == items.length){
                notFull.await();//等待非满
            }
            items[tail] = t;
            tail ++;
            if(tail == items.length){
                tail = 0;
            }
            count++;
            notEmpty.signal();//唤醒那些执行take()而阻塞的线程
            
        }finally{
            lock.unlock();
        }
    }
    //阻塞,一直到notEmpty
    public T take() throws InterruptedException {
        lock.lock();
        try{
            while(count == 0){
                notEmpty.await();//等待非空
            }
            T t = items[head];
            items[head] = null;
            head ++;
            if(head == items.length){
                head = 0;
            }
            count--;
            notFull.signal();//唤醒那些执行put()而阻塞的线程
            return t;
        }finally{
            lock.unlock();
        }
    }
}

至此,上面的所有的问题已经全部完美的得到了解决!

希望以上对你理解wait&notify,lock&condition有所帮助,也欢迎大家观看我的两个视频课程:Java生产环境下性能监控与调优详解 Java秒杀系统方案优化 高性能高并发实战

12人推荐
随时随地看视频
慕课网APP