likes
comments
collection
share

从有点基础开始学JUC:线程间定制化通信

作者站长头像
站长
· 阅读数 15

线程间定制化通信

概念

多个线程之间的执行时机并不是固定的,是由 CPU 来操作调度的,而如果要让线程按照我们需要的顺序执行,那这就是线程间定制化通信

实现

因为一般的notify()notifyAll()方法唤醒的线程具有强烈的随机性,所以我们这里采用Condition接口

Condition接口的特殊之处在于:

相较于synchronized只能与一个对象的监视器相关联,而ReentrantLock对象,则可以通过反复调用newCondition()方法,创建多个Condition对象而这些Condition对象则与lock的监视器相对应,可以自由的唤醒对应的Condition对象的线程

Condition

概述

在开始写正式的代码之前,我们先来补充一些Condition的基础知识

每个Condition对象都包含一个队列(等待队列)。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态

常用方法

方法描述
await()造成当前线程在接到信号或被中断之前一直处于等待状态。
boolean await(long time, TimeUnit unit)造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态->是否超时,超时异常
awaitNanos(long nanosTimeout)造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
awaitUninterruptibly()造成当前线程在接到信号之前一直处于等待状态。(注意:该方法对中断不敏感)。
awaitUntil(Date deadline)造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
signal()唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
signalAll()唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

原理简述

每个Condition对象都包含一个队列(等待队列)。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。等待队列的基本结构如下所示:

从有点基础开始学JUC:线程间定制化通信

注意:

  • 等待队列分为首节点和尾节点。当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。
  • 新增节点就是将尾部节点指向新增的节点。节点引用更新本来就是在获取锁以后的操作,所以不需要CAS保证。同时也是线程安全的操作
  • 调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点)

代码总览

现在,我们来看一个代码

public class Restaurant {

    private final Lock lock = new ReentrantLock();

    private final Condition customerCondition = lock.newCondition();

    private final Condition waiterCondition = lock.newCondition();

    private final Condition chefCondition = lock.newCondition();

    private boolean isOrderReceived = false;
    private boolean isMealServed = false;

    public void reception() {
        lock.lock();
        try {
            System.out.println("服务生接待顾客");

            customerCondition.signal();

            while (!isOrderReceived) {
                waiterCondition.await();
            }

            System.out.println("顾客点餐完毕,服务生给厨师菜单");

            chefCondition.signal();

            while (!isMealServed) {
                waiterCondition.await();
            }

            System.out.println("将饭菜给顾客");

            customerCondition.signal();

            while (isMealServed) {
                waiterCondition.await();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void orderFood(){
        lock.lock();
        try {
            System.out.println("用户点餐");

            isOrderReceived = true;
            waiterCondition.signal();

            while (!isMealServed) {
                customerCondition.await();
            }

            System.out.println("顾客用餐");

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void cook() {
        lock.lock();
        try {
            System.out.println("厨师做饭");

            isOrderReceived = false;
            isMealServed = true;
            waiterCondition.signal();

            while (isOrderReceived) {
                chefCondition.await();
            }

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}

这里我们使用使用newCondition()方法新建了三个Condition对象,同时我们定义了两个标志变量:isOrderReceivedisMealServed,分别表示顾客是否已经点餐饭菜是否已经送到顾客手中。然后是三个方法,分别是reception()(接待)、orderFood()(点餐)、cook()(做饭),用于模拟服务生接待顾客、顾客点餐吃饭、厨师做饭。

接下来,我们看看这三个代码的具体执行流程

具体执行流程

  1. 首先,我们要执行reception()方法

  2. 然后服务生要先接待顾客,然后则到了点餐的时候,所以这时候要先唤醒customerCondition对象的线程(也就是顾客执行操作的线程),然后判断顾客是否已经点餐了,如果还没有点餐,则使自身线程休眠

    System.out.println("服务生接待顾客");
    
    customerCondition.signal();
    
    while (!isOrderReceived) {
        waiterCondition.await();
    }
    
  3. 然后则是用户点餐,点餐完成之后就到了服务生将菜单给厨师,让厨师做菜,所以首先要让isOrderReceived标志变量为true,然后重新唤醒waiterCondition对应的线程,最后,在服务生未将饭菜给顾客之前,要保持线程的休眠

    System.out.println("用户点餐");
    
    isOrderReceived = true;
    waiterCondition.signal();
    
    while (!isMealServed) {
        customerCondition.await();
    }
    
  4. 接着是服务生将菜单给厨师,所以要唤醒chefCondition对应的线程,同时保持自身线程的休眠

    System.out.println("顾客点餐完毕,服务生给厨师菜单");
    
    chefCondition.signal();
    
    while (!isMealServed) {
        waiterCondition.await();
    }
    
  5. 然后是厨师根据菜单做菜,首先要将isOrderReceived设置为false,表示当前菜单已经处理了,和isMealServed设置为true,表示厨师已经做出饭菜,接着唤醒waiterCondition对应的线程,让服务生将饭菜给顾客,同时在下一份饭菜来之前,保持自身线程的休眠

    System.out.println("厨师做饭");
    
    isOrderReceived = false;
    isMealServed = true;
    waiterCondition.signal();
    
    while (isOrderReceived) {
        chefCondition.await();
    }
    
  6. 服务生受到厨师做完菜的消息后,将食物给顾客,同时唤醒customerCondition对应的线程,让顾客消费,同时保持自身线程的休眠

    System.out.println("将饭菜给顾客");
    
    customerCondition.signal();
    
    while (isMealServed) {
        waiterCondition.await();
    }
    
  7. 最后,顾客用餐

    System.out.println("顾客用餐");
    

执行代码

然后,来看我们的运行的代码:

public class RestaurantTest {

    public static void main(String[] args) {

        Restaurant restaurant  = new Restaurant();

        new Thread(restaurant::reception, "reception").start();
        new Thread(restaurant::orderFood, "orderFood").start();
        new Thread(restaurant::cook, "cook").start();
    }

}

新建三个线程执行代码,下面是执行的结果:

服务生接待顾客
用户点餐
顾客点餐完毕,服务生给厨师菜单
厨师做饭
将饭菜给顾客
顾客用餐

这就是这篇文章的内容了,欢迎大家的讨论,如有错漏,也请指出,谢谢~