JUC并发编程——Condition/Object中阻塞与唤醒的原理

175 阅读4分钟

摘要

各位小伙伴对Object中的方法wait()和notify()都是有所耳闻但,但是很少人对JUC中的Condition接口来实现线程间通讯,大家可能就很少了解,本博文将介绍Condition接口的通信原理,帮助大家学习和了解更多的线程通信方式。

一、线程的唤醒

wait() 与 notify() 和 notifyAll()

  1. wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
  2. notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
  3. notifyAll ():唤醒正在排队等待资源的所有线程结束等待。 说明:
  4. wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
  5. wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。 否则,会出现IllegalMonitorStateException异常
  6. wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

二、虚假唤醒(notifyAll)的问题

  1. 生产者(Productor)将产品交给店员(Clerk)
  2. 而消费者(Customer)从店员(Clerk)处取走产品
  3. 店员(Clerk)一次只能持有固定数量的产品(比如:1)
  4. 如果生产者试图生产更多的产品,店员会叫生产者停一下 (wait) ,如果店中有空位放产品了再通知 (notifyAll) 生产者继续生产
  5. 如果店中没有产品了,店员会告诉消费者等一下 (wait),如果店中有产品了再通知 (notifyAll) 消费者来取走产品。
// 店员类
class Clerk {
 
    //定义商品的数量
    private int product = 0; // 当前商品的数量
    private int total = 10; // 货架允许存放的商品总数量
 
    //进货的同步方法:提供给生产者线程调用
    public synchronized void get() {
        if (product > total) {
            // 如果商品超出 货架允许存放商品的数量,则提示产品已摆满!
            System.out.println("产品已摆满!");
        } else {
            // 如果还可以摆放产品,则继续摆放,设置产品的数量++,然后打印出来
            System.out.println(Thread.currentThread().getName() + ":" + ++product);
        }
    }
 
    //卖货的同步方法:提供给消费者线程调用
    public synchronized void sale() {
        if (product <= 0) {
            //如果商品数量小于0,说明缺货了
            System.out.println("缺货了");
        } else {
            //如果商品数量大于0,说明还有商品可以卖(商品的数量--)
            System.out.println(Thread.currentThread().getName() + " :" + --product);
        }
    }
}
//生产者
class Productor implements Runnable {
    //定义员工类对象
    private Clerk clerk;
 
    //构造器:接收传递的员工类对象
    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
 
    //生产者线程
    @Override
    public void run() {
        //循环20次,调用员工收货,增加货物的数量
        for (int i = 0; i < 20; i++) {
            // 休眠200毫秒
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
            }
            //调用员工收货
            clerk.get();
        }
    }
}
//消费者
class Consumer implements Runnable {
    //定义员工类对象
    private Clerk clerk;
 
    //构造器:接收传递的员工类对象
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
 
    //消费者线程
    @Override
    public void run() {
        //循环20次,调用员工卖货,减少货物的数量
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}
public class TestProductorAndConsumer {
 
    public static void main(String[] args) {
        //创建员工类对象
        Clerk clerk = new Clerk();
        //创建生产者线程类对象
        Productor productor = new Productor(clerk);
        //创建消费者线程类对象
        Consumer consumer = new Consumer(clerk);
 
        //创建线程
        new Thread(productor, "生产者A").start();
        new Thread(consumer, "消费者B").start();
    }
}

image.png

存在的问题:消费者就算没货,依然不断去消费;生产者已经摆满产品,依然不断去生产产品 那么怎么去解决这个问题呢?这个时候,就需要使用线程的通信 wait() 和 notifyAll() 的方法来处理了。处理思路如下:

  • 生产者:当生产已经摆满产品,那么就设置线程阻塞 wait() ;反之,使用 notifyall() 开启生产
  • 消费者:当产品已经缺货,那么久设置线程阻塞 wait(); 反之,使用 notifyall() 开启消费
// 店员类
class Clerk {
 
    //定义商品的数量
    private int product = 0; // 当前商品的数量
    private int total = 10; // 货架允许存放的商品总数量
 
    //进货的同步方法:提供给生产者线程调用
    public synchronized void get() {
        if (product > total) {
            // 如果商品超出 货架允许存放商品的数量,则提示产品已摆满!
            System.out.println("产品已摆满!");
            // 已经摆满货物 需要停止生产 设置阻塞线程
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        } else {
            // 反之 开始生产 唤醒线程操作
            this.notifyAll()
            // 如果还可以摆放产品,则继续摆放,设置产品的数量++,然后打印出来
            System.out.println(Thread.currentThread().getName() + ":" + ++product);
        }
    }
 
    //卖货的同步方法:提供给消费者线程调用
    public synchronized void sale() {
        if (product <= 0) {
            //如果商品数量小于0,说明缺货了
            System.out.println("缺货了");
            // 已经缺货了 需要停止消费 设置阻塞线程
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        } else {
            // 反之 开始生产 唤醒线程操作
            this.notifyAll()
            //如果商品数量大于0,说明还有商品可以卖(商品的数量--)
            System.out.println(Thread.currentThread().getName() + " :" + --product);
        }
    }
}

image.png

### 当我们将生产产品的总数量调小为 1,双线程将会无法停止执行。根本的原因是最后一个 wait 没有被唤醒

private int total = 1; // 货架允许存放的商品总数量

一、Condition底层模型

image.png

image.png

image.png

二、Condition接口与wait()和notify()比较

Condition底层的线程等待也是使用的LockSupport的park方法。它对interrupt方法不敏感,而wait方法遇到interrupt就抛异常。

对比项Object Monitor MethodsCondition
前置条件获取对象锁调用Lock.lock() 获取锁 、调用的Lock.lock() 获取Condition对象
调用方式直接调用object.wait()直接调用condition.await()
等待队列一个多个
当前线程释放锁并进入等待队列支持支持
等待队列不响应中断不支持支持
超时等待状态支持支持
唤醒队列中的一个线程支持支持
唤醒队列的全部线程支持支持

await方法,其实就是先释放锁(说白了底层就是更改了AQS的state),然后调用LockSupport的park方法park自己,再把自己丢进Condition的等待队列中。这里不要忘记了,释放锁的同时还通知了同步队列中的线程去拿锁哦。 signal方法其实就是将Condition等待队列中的线程移动到同步队列。至于它拿不拿的到锁,还是另外一回事。

博文参考