线程间的协作-wait/notify与condition

线程间的协作

有些时候,线程之间需要相互配合,完成某项工作。
例如:A线程修改了一个对象的值,而B线程想要感知这个变化,然后进行相应的操作。整个过程中,A线程可以看成是“生产者”,B线程可以看成是消费者。
在这个场景中,简单的方式是让消费者线程不断地循环检查变量是否符合预期。但这种方式会存在以下弊端:

  • 难以保证及时性
  • 资源开销大,自旋会浪费cpu资源

因此,我们需要寻找新的方式来解决线程之间的协作问题。
显然,jdk已经为我们考虑在内了。解决此问题的方法在java中有以下两种方式:

  1. 基于内置锁synchronized,通过Object提供的wait方法以及notify、notifyAll方法来实现。
  2. 基于显示锁,通过Condition对象的await方法和signal()/signaAll方法来实现。

基于内置锁的线程间的通信

Java的 Object类本身就是监视者对象,Java对于Monitor Object模式做了内建的支持:

  • 每个 Object 都带了一把看不见的锁,通常叫 Monitor锁, 这把锁就是监控锁
  • synchronized 关键字修饰方法和代码块就是 同步方法
  • wait()/notify()/notifyAll()方法构成监控条件(Monitor Condition)
    下图描述了 Java Monitor 的工作远理:
    monitor-lock
  1. 线程进入同步方法中
  2. 为了继续执行临界区代码,线程必须获取Monitor锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)。
  3. 拥有监视者对象的线程可以调用wait()进入等待集合(WaitSet),同时释放监视锁,进入等待状态。
  4. 其他线程调用notify()/notifyAll()接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
  5. 同步方法执行完毕了,线程退出临界区,并释放监视锁。

wait()

调用该方法的线程会进入WAITING状态,只有等待其他线程的通知或者被中断才会返回。调用wait()方法后,会释放对象的锁,进入等待池。
既然是释放对象的锁,那么肯定先要获得锁,所以wait是写在synchronized中的,否则会报异常。

wait(long time)/wait(long time,int nanos)

超时等待一段时间,如果没有通知则返回。

notify()

通知一个对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁。
notify()调用时并不会真正释放对象锁,必须等到synchronized方法或者语法块执行完才真正释放锁。举个例子:

Object object = new Object();
synchronized (object) {
    object.notify();
    while (true){
    }
}

如上, 虽然调用了notify,但是后面进入了一个死循环。这会一直不能释放对象锁。
所以,即使它把在等待池中的线程唤醒放到了对象的锁池中,但是锁池中的线程不会运行,因为它始终拿不到锁。

notifyAll()

通知所有等待在该对象上的线程。

代码示例

public static void main(String[] args) {
    final Object innerLock = new Object();

    new Thread(() -> {
        System.out.println("thread 0 init...");
        synchronized (innerLock) {
            try {
                System.out.println("thread 0 get lock...");
                System.out.println("thread 0 start exe wait()...");
                innerLock.wait();
                System.out.println("thread 0 exe wait() end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    new Thread(() -> {
        System.out.println("thread 1 init...");
        synchronized (innerLock) {
            System.out.println("thread 1 get lock...");
            innerLock.notify();
            System.out.println("thread 1 exe notify() end...");
        }
    }).start();
}
thread 0 init...
thread 0 get lock...
thread 0 start exe wait()...
thread 1 init...
thread 1 get lock...
thread 1 exe notify() end...
thread 0 exe wait() end...

根据上面的案例,我们可以得出大致流程是:

  • lock对象维护了一个等待队列queue;
  • 线程0中执行lock的wait方法,把线程0保存到queue中;
  • 线程1中执行lock的notify方法,从等待队列中取出线程0继续执行;

基于显示锁的线程间的通信

基于显示锁实现的线程间同步机制是通过Condition接口的await()方法和signal()/signalAll()方法实现的。
Condition是在jdk 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作。阻塞队列也是使用了Condition来模拟线程间的协作。

内置Monitor锁缺陷

过早唤醒

wait 方法的返回并不一定意味着线程正在等待的条件谓词已经成真。考虑下图的阻塞队列代码段:
过早唤醒
假设有A,B两个线程阻塞在put函数,C线程调用take,获取并推出队列中一个数据,同时调用notifyAll唤醒A,B线程;若A线程获取到了内置锁,B阻塞在锁获取中,A 又向队列压入一个数据,此时队列又满了;A释放锁后,B线程获取锁,但是队列已满,条件谓词判断失败,再次 wait 阻塞。

信号丢失

信号丢失是指:线程正在等待一个已经发生过的唤醒信号。错误的编程模式通常会造成信号丢失。考虑图 3 的阻塞队列代码段:
信号丢失
假设有A线程阻塞在put函数,B线程阻塞在take函数,C线程调用take,然后使用 notify接口唤醒其中一个线程;不巧的是B线程被唤醒,B检查队列仍然为空,继续等待阻塞,此时应该被唤醒的 A 只能等待下一次的唤醒。

Condition

内置Monitor锁的过早唤醒和信号丢失,它们其实有一个共同的原因:多个线程在同一个条件队列上等待不同的条件谓词。
如果想编写一个带有多个条件谓词的并发对象,或者想拥有除了条件队列可见性以外的更多控制权,就可以使用显示的Lock和Condition而不是内置锁和条件队列。
与内置条件队列不同,对于每一个Lock,可以有任意数量的Condition对象,因此对于不同的条件谓词,对于同一个锁,可以用不同的Condition对象来控制。
Condition也提供了丰富的接口等待挂起(可轮询,可中断,可超时等)。接口如下所示:

void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;

void signal();
void signalAll();
代码示例
public static void main(String[] args) {
    //锁对象
    final Lock lock = new ReentrantLock();
    final Condition writeCond = lock.newCondition();//写线程条件变量
    final Condition readCond = lock.newCondition();//读线程条件变量

    final LinkedList<V> items = new LinkedList<V>();
    final int totalCount = 100;

    public void put (V x) throws InterruptedException {
        lock.lock();
        try {
            //如果队列满了
            while (totalCount >= items.size()) {
                writeCond.await();//阻塞写线程
            }

            items.addLast(x);

            readCond.signal();//唤醒读线程
        } finally {
            lock.unlock();
        }
    }

    public V take () throws InterruptedException {
        lock.lock();
        try {
            //如果队列为空
            while (items.size() == 0) {
                readCond.await();//阻塞读线程
            }

            V x = items.removeFirst();
            writeCond.signal();//唤醒写线程
            return x;
        } finally {
            lock.unlock();
        }
    }
}

在put的时候写入数据,signal只会唤醒等待读的线程;对应的take取出数据后,唤醒的也只会是等待写的线程。
Condition比内置锁做的更加细致,能够很好的解决过早唤醒和信号丢失的问题。

https://www.jianshu.com/p/6fe4bc3374a2
https://www.jianshu.com/p/ffc0c755fd8d
http://jianshu.com/p/1c52f17efaab
https://www.cnblogs.com/dolphin0520/p/3920385.html
https://hellofrank.github.io/2020/06/22/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF-%E5%9B%9B-%E2%80%94%E2%80%94%E7%BA%BF%E7%A8%8B%E9%97%B4%E7%9A%84%E5%8D%8F%E4%BD%9C/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值