Java多线程(五)——线程等待/通知机制之条件变量(二)

Posted by Lain on 10-31,2019

0

上一章节的最后,我们提到了 Object 提供的 wait/notify 的一些不足,主要有以下几点:

  1. notify 只会随机唤醒等待在锁上的任意一个线程,并不一定是我们想要唤醒的。
  2. notifyAll 会将全部等待在锁上的线程唤醒,虽然 while(!保护条件)代码块保证了在全部唤醒线程时,被提前唤醒的线程不会执行出错,但频繁的锁争夺会照成上下文频繁切换,对性能造成不利影响。
  3. Object.wait(long)无法判断该方法的返回是因为被唤醒引起的,或是因为超时引起的。

正是因为 wait/notify 的这些特性,使得我们在使用原生的锁时,要尽量保证一个对象锁住的是同质线程,即保护条件和执行逻辑都一致的线程,否则就应该尽量使用 notifyAll。

在 JDK1.5 中,引入了新的类库 Condition 来代替原生的 Object.wait()/notify()来实现线程的等待与通知机制,并解决了上面所提到的问题。

java.util.concurrent.locks.Condition 来自 concurrent 包。通过 Lock.newCondition()可以返回一个相应的 Condition 实例。和 Object.wait()/notify()一样,Object 的等待通知都要求执行线程必须持有相应的对象的内部锁,Condition.await()/signal()则要求执行线程持有创建该 Condition 实例的显式锁。

关于显式锁,我之后会专门写一篇笔记进行介绍,这里只要知道 Condition 是来自 concurrent 包的特性就行,它由显式锁 Lock 对象创建,在调用 Condition 的相关方法时,当前执行线程必须持有该 Lock 对象的锁。

Condition 就是我们说的条件变量,或叫做条件队列,每个 Condition 实例都会维护一个线程队列,当一个线程调用了 Condition 实例的 await()方法时,都会被暂停并将之加入到该 condition 的线程队列中,每当有其他线程调用了 Condition 实例的 signal()方法时,会将该实例的等待队列中的任意一个线程唤醒,当调用其 signalAll()时,会将等待队列中的所有线程全部唤醒。

有的人就可能会问了(说,这个人是不是你自己.jpg)这样看上去不是和原生的没有区别吗 Kora!

对的,对于单个 Condtion 来说,它的使用和原生的没有区别。然而同一个 Lock 对象,是可以创建出多个 Condition 的,而多个 Condition 之间的等待/唤醒都相互隔离,比如我通过同一个显式锁,创建了两个 Condition 对象,cond1、cond2,我对 cond1 进行 signalAll,只会唤醒等待在 cond1 上的所有线程,而对 cond2 没有任何影响,而 cond1、cond2 均要求线程持有同一个显式 Lock,也就是说,我们现在可以通过同一个锁,来实现对多类保护条件不相同的线程的等待/通知管理。

Codition 的使用和 Object 的 wait/notify 类似:

class Lain{
    private final Lock lock= new ReentrantLock();//显式锁
    private final Condition condition = lock.newCondition();
    private static boolean flag = false;//共享变量(保护条件)

//等待线程调用
    public void locked(){
        lock.lock();
        try {
            while(!flag){//防止欺骗性唤醒、信号丢失
                condition.await();
            }
            doAction();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void doAction(){
        System.out.println("action");
    }

//唤醒线程调用
    public void notifyIt(){
        lock.lock();//由同一个显式锁锁住
        try {
            this.flag = true;//更新共享变量
            condition.signal();//唤醒线程
        }finally {
            lock.unlock();
        }

    }

}
tip:显示锁的释放一定要放在finally块中,以保证其必定被执行

那么它是怎么解决提前唤醒的问题的呢?很明显,如果有三个线程 T1、T2、T3,其中 T1、T2 线程都是由同一个保护条件所保护的,T3 是由其他保护条件所保护的,那么对于 T1,T2,我们可以使用锁对象 lock 创建一个条件变量 cond1,对于 T3 我们同样创建一个新的条件变量 cond2,对于这两类线程,我们分别使用不同的条件变量进行等待/通知操作,这样对于 T1、T2,当我们想要唤醒他们的时候,只需要调用 cond1 的 signalAll 方法,而 T3 并不会因此被唤醒,进入抢夺锁的过程中去,避免了其被提前唤醒。

Condition 接口只是为我们解决提前唤醒的问题提供了支持,实际在使用的时候,应当自行在代码中维护条件变量和保护条件之间的关系,也就是同样的保护条件的线程,应当调用同一个条件变量的 await 方法去实现该类线程的等待,而在通知线程更新保护条件,即更新了相关共享变量后,使用同一个条件变量的 signal/signalAll 方法进行唤醒通知。

Condtion还解决了Object.wait(long timeout)带来的问题,即原生方法不能判断线程是因为超时而被唤醒的,还是因为被通知了而被唤醒的。Condition.awaitUntil(Date deadLine)的返回值使得我们能进行该项判断。deadLine表示的是等待的最后期限,如果在最后期限之前被唤醒,awaitUntill将会返回true。Condition.await()/awaitUntil(Date)和Object.wait()/wait(long)一样,当等待线程被await/awaitUntil暂停时,线程会将其相应的显式锁释放,等到线程被唤醒,或到达deadline,线程需要再次获得该显式锁,才能将等待方法返回。在唤醒——获得锁的时间里,共享变量有可能再次被其他线程所更改,使得其不能满足保护条件,所以awaitUntil也要置于while(!保护条件)中,当我们发现awaitUntil返回了true时,我们需要对保护条件再次进行判断,直到保护条件成立,执行之后的业务代码,或者直到超时,整个方法直接返回。

本篇中我们第一次接触到了显式锁,以及concurrent并发包,下一篇笔记我们将来学习显示锁与内部锁的区别与选用。