6.condition条件变量提高线程通信效率

297 阅读8分钟

条件变量实现原理

Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。

Condition类能实现synchronized和wait、notify搭配的功能,另外比后者更灵活,Condition可以实现多路通知功能,也就是在一个Lock对象里可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活

而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在这个对象上。线程开始notifyAll时,需要通知所有的WAITING线程,没有选择权,会有相当大的效率问题。

一种比较流行的说法就是,除非深思熟虑,否则尽量使用 notifyAll()

我们今天就这个问题,来讨论一下这两个方法如何选择。

等待-通知机制

在此之前,先来聊聊什么是等待-通知机制,以及它能解决什么问题?

在使用并发编程时,利用多线程来提高任务的执行效率,但是每个线程在执行时,都有一些先决条件需要被满足。

例如生产者消费者模式下,消费者线程能够执行的先决条件,就是生产者产生了一个待消费的数据。

那么如果线程要求的条件,不满足时,循环等待是一种方案,循环间隔一段时间,再重新尝试验证条件是否满足。

但是这样的循环等待,在某些场景下,可能会循环很多次,导致大量消耗 CPU 资源[自旋也是如此]

更好的方案,则是等待-通知机制。当线程要求的条件不满足时,主动进入等待状态,等线程等待的条件,再次被满足后,通知这个等待的线程重新执行

这就解决了 CPU 资源,因为循环而导致消耗的问题。

对标到 Synchronized 中,被 Synchronized 标记的代码块,被称为临界区,在同一时刻,只有一个线程能过获取 Synchronized 的互斥锁,进入临界区执行。

线程处于临界区时,一旦发现执行条件不满足时,则可以调用wait()或者wait(time_out)方法,进入等待,例如消费者发现没有更多需要处理的数据,此时就调用wait()方法,进入等待,等待生产者产生一条新的待消费数据。接下来如果生产者线程,产生了一个新的数据后,就需要唤醒之前等待的消费者线程,去处理这条数据。这里的唤醒操作,就是调用notify()或者notifyAll()`方法。

可以看到,notify()和?notifyAll()的作用是一致的,都是去唤醒等待中的线程。但是也正如他们方法名所描述的,notify()会"随机"唤醒一个等待线程,而notifyAll()会尝试唤醒所有的等待中的线程。

注意这里的唤醒,并不是真的唤醒去执行,实际上只是让处于等待的线程,有重新获取锁的争抢权,也就是说,哪怕此时有一百个线程处于等待状态,此时调用notifyAll()也只会有一个线程获取到锁,允许进入临界区执行。

这在底层中,其实是利用了两个等待队列来实现的,分别是入口队列(EntrySet)和等待队列(WaitSet)。

image.png

被Synchronized 阻塞等待获取锁的线程,会进入入口队列

而当条件不满足时,主动调用wait()方法进入等待的线程,则会进入等待队列

在等待队列中的线程,如果不被唤醒,则永远没有锁的争抢权,无法获取锁也就无法被执行。

尽量使用notifyAll

终于进入主题了,就前面的描述,看似应该是使用notify()更好一些。因为即便我们通知了等待队列中,所有的线程,但同一时刻,也只有一个线程可以获取互斥锁,进入临界区执行,这么看来notify()会更高效一些。

但是这里埋下来一个风险,就是只使用 notify()可能会导致某些线程,一直处于等待队列中,而永远不会被唤醒并获得执行权,也就是饥饿

理想情况下,一次等待(wait)对应一次通知(notify),是非常完美的,但是实际业务场景下,可能做不到。

举个例子,在多生产者消费者模式下,待处理的数据队列只有一条数据了,理想场景下,消费者在处理掉一条数据后,理论上应该唤醒生产者再生产一条新的待消费数据。可是notify()是随机唤醒,也就是它可能会唤醒一个消费者线程,这个消费者线程,发现没有待处理的数据,此时条件不满足,又主动进入等待队列。

也正是因为如此,在并发编程中有个范式模板:

synchronized(this){
    while(条件不满足){
        wait();
    }
    //do something
}

这段代码,大家应该很熟悉,notify()只能保证唤醒一个线程,但是不保证线程执行的时候,曾经的等待条件已经被满足了。

为了保证可靠性,此处使用循环检测的方式,只有必要条件满足时,才继续执行。

正是因为notify()随机唤醒的特点,导致在多条件的情况下,会导致某些线程永远不会被通知到。

稳妥的方式,是使用notifyAll(),让等待中的线程,都有一次再执行的权利。

这也就是为什么说,除非深思熟虑,否则尽量使用 notifyAll()

条件变量提高线程通信效率

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

1.Condition是个接口,基本的方法就是await()和signal()方法。

2.Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()。

3.调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。

4.Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现。

条件变量类似JDK1.4或以前版本中的 Object.wait(); Object.notify(); Object.notifyAll();

值得注意的是当condition.await()时,隐式的将条件变量关联的Lock解锁,而使其他线程有机会获得Lock,而检查条件,并在条件满足时,等待在条件变量上。

有多个线程往里面存数据和从里面取数据,其队列(先进先出后进后出)能缓存的最大数值是capacity,多个线程间是互斥的。

当缓存队列中存储的值达到capacity时,将写线程阻塞,并唤醒读线程,当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程。

这就是多个Condition的强大之处。

假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。

那么假设只有一个Condition会有什么效果呢,缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间!Synchronized的notify也会存在类似的问题,不能精准唤醒。

应用-线程同步

public class TestCondition {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
​
        new Thread(() -> {
            System.out.println("1 start");
            lock.lock();
            try {
                System.out.println("1 entry");
                System.out.println("1A await");
                conditionA.await();
                System.out.println("1B signal");
                conditionB.signal();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("1 entry finish");
            try {
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        
        //为保证A能先获取到锁
        Thread.sleep(2000);
        
        new Thread(() -> {
​
            System.out.println("2 start");
            lock.lock();
            System.out.println("2A signal");
            conditionA.signal();
            try {
                System.out.println("2 entry");
                System.out.println("2B await");
                conditionB.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("2 entry finish");
            try {
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}
输出:
A start
A entry
A await
B start
B signal
B entry
B await
signal B
A entry finish
B entry finish

Condition数据结构

image.png

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject.

 public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
​
        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }
            
     
         public final void await() throws InterruptedException {
             if (Thread.interrupted())
                 throw new InterruptedException();
             Node node = addConditionWaiter();
             int savedState = fullyRelease(node);
             int interruptMode = 0;
             while (!isOnSyncQueue(node)) {
                 LockSupport.park(this);
                 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                     break;
             }
             if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                 interruptMode = REINTERRUPT;
             if (node.nextWaiter != null) // clean up if cancelled
                 unlinkCancelledWaiters();
             if (interruptMode != 0)
                 reportInterruptAfterWait(interruptMode);
         }
     
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                //只有1个指针nextWaiter
                //说明是1个单向队列
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
}

只有1个指针nextWaiter 可以发现是一个单向链表