Java多线程中的wait/notify通信模式实例详解

前言

最近在看一些JUC下的源码,更加意识到想要学好Java多线程,基础是关键,比如想要学好ReentranLock源码,就得掌握好AQS源码,而AQS源码中又有很多Java多线程经典的一些应用;再比如看了线程池的核心源码实现,又学到了很多核心实现,其实这些都可以提出来慢慢消化并变成自己的知识点,今天这个Java等待/通知模式其实是Thread.join()实现的关键,还有线程池工作线程中线程跟线程之间的通信的核心所在,故在此为了加深理解,做此记录!

参考资料《Java并发编程艺术》(电子PDF版)

一、什么是Java线程的等待/通知模式

1、等待/通知模式概述

  首先先介绍下官方的一个正式的介绍:

  等待/通知机制,是指一个线程A调用了对象object的wait()方法进入等待状态,而另一个线程B调用了对象object的notify或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而还行后续操作。

  而我的理解是(举例说明):

  假设工厂里有两条流水线,某个工作流程需要这两个流水线配合完成,这两个流水线分别是A和B,其中A负责准备各种配件,B负责租装配件之后产出输出到工作台。B的工作需要A的配件准备充分,否则就会一直等待A准备好配件,并且A准备好配件后会通过一个开头通知告诉B我已经准备好了,你那边不用一直等待了,可以继续执行任务了。流程A与流程B就是对应的线程A与线程B之间的通信,即可以理解为相互配合,具体也就是“”通知/等待“”机制!

2、需要注意的细节  

  那么,我们都知道超类Object有wait()方法与notify()/notifyAll()方法,在进行正式代码举例之前,应该先加深下对这三个方法的理解与一些细节(有一些细节确实容易被忽略)

  • 调用wait()方法,会释放锁(这一点我想大部分人都知道),线程状态由RUNNING->WAITNG,当前线程进入对象等待队列中;
  • 调用notify()/notifyAll()方法不会立马释放锁(这一点我大家人也应该知道,但是什么时候释放锁呢?--------请看下一条),notify()方法是将等待队列中的线程移到同步队列中,而notifyAll()则是全部移到同步队列中,被移出的线程状态WAITING-->BLOCKED;
  • 当前调用notify()/notifyAll()的线程释放锁了才算释放锁,才有机会唤醒wait线程返回(为什么有才有机会返回呢?------继续看下一条)
  • 从wait()返回的前提是必须获得调用对象锁,也就是说notify()与notifyAll()释放锁之后,wait()进入BLOCKED状态,如果其他线程有竞争当前锁的话,wait线程继续争取锁资格(不好理解的话,请看下面的代码举例)
  • 使用wait()、notify()、notifyAll()方法时需要先调对象加锁(这可能是最容易忽视的点了,至于为什么,请先看了代码之后,看本篇博文最后补充:wait()、notify()、notifyAll()加锁的原因----防止线程即饥饿)

二、代码举例

1、结合代码理解

结合上述的“工厂流程装配配件并产出的例子”,我们有两个线程(流水线)WaitThread与NotifyThread、其中WaitThread是被通知的任务,完成主要的工作(组装配件完成产品),需要时刻判断标志位(开关);NotifyThread是需要通知的任务,需要对WaitThread进行“监督通知”,两个配合才能更好完成产品的组装并输出。

public class WaitNotify {

 static Object lock = new Object();
 static boolean flag = false;
 public static void main(String[] args) {
  new Thread(new WaitThread(), "WaitThread").start();
  try {
   Thread.sleep(1000);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  new Thread(new NotifyThread(), "NotifyThread").start();

 }

 /**
  * 流水线A,完成主要任务
  */
 static class WaitThread implements Runnable{
  @Override
  public void run() {
   // 获取object对象锁
   synchronized (lock){
    // 条件不满足时一直在等,等另外的线程改变该条件,并通知该wait线程
    while (!flag){
     try {
      System.out.println(Thread.currentThread() + " is waiting, flag is "+flag);
      // wait()方法调用就会释放锁,当前线程进入等待队列。
      lock.wait();
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
    // TODO 条件已经满足,不继续while,完成任务
    System.out.println(Thread.currentThread() + " is running, flag is "+flag);
   }
  }
 }
 /**
  * 流水线B,对开关进行控制,并通知流水线A
  */
 static class NotifyThread implements Runnable{
  @Override
  public void run() {
   // 获取等wait线程同一个object对象锁
   synchronized (lock){
    flag = true;
    // 通知wait线程,我已经改变了条件,你可以继续返回执行了(返回之后继续判断while)
    // 但是此时通知notify()操作并立即不会释放锁,而是要等当前线程释放锁
    // TODO 我准备好配件了,我需要通知全部的组装流水线A.....
    lock.notifyAll();
    System.out.println(Thread.currentThread() + " hold lock, notify waitThread and flag is "+flag);
   }
  }
 }
}

运行main函数,输出:

Thread[WaitThread,5,main] is waiting, flag is false
Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true
Thread[WaitThread,5,main] is running, flag is true

车床流水工作开启,流水线的开关一开始是关闭的(flag=false),流水线B(NotifyThread)去开启后,开始自动唤醒流水线A(WaitThread),整个流水线开始工作了......

  • Thread[WaitThread,5,main] is waiting, flag is false: 一开始流水线A发现自己没有配件可租装,所以等流水线A准备好配件(这样是不是觉得特别傻,哈哈哈,真正的流水线不会浪费时间等的,而且会有很多条流水线B准备配件的,这里只是举例说明,望理解!);
  • Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true:流水线B准备好了配件,开启开关(flag=ture),并通知流水线A,让流水线A开始工作;
  • Thread[WaitThread,5,main] is running, flag is true,流水线B收到了通知,再次检查开关是否开启了,开启的话就开始返回继续完成工作了。

其实结合上述我举的例子还是很好理解的,下面是大概的一个粗略时序图:

2、扩展理解----wait()返回的前提是获得了锁

上述已经表达了这个注意的细节:从wait()返回的前提是必须获得调用对象锁,我们再增加能竞争lock的同步代码块(红字部分)。

public class WaitNotify {

 static Object lock = new Object();
 static boolean flag = false;
 public static void main(String[] args) {
  new Thread(new WaitThread(), "WaitThread").start();
  try {
   Thread.sleep(1000);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  new Thread(new NotifyThread(), "NotifyThread").start();
 }

 /**
  * 流水线A,完成主要任务
  */
 static class WaitThread implements Runnable{
  @Override
  public void run() {
   // 获取object对象锁
   synchronized (lock){
    // 条件不满足时一直在等,等另外的线程改变该条件,并通知该wait线程
    while (!flag){
     try {
      System.out.println(Thread.currentThread() + " is waiting, flag is "+flag);
      // wait()方法调用就会释放锁,当前线程进入等待队列。
      lock.wait();
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
    // TODO 条件已经满足,不继续while,完成任务
    System.out.println(Thread.currentThread() + " is running, flag is "+flag);
   }
  }
 }
 /**
  * 流水线B,对开关进行控制,并通知流水线A
  */
 static class NotifyThread implements Runnable{
  @Override
  public void run() {
   // 获取等wait线程同一个object对象锁
   synchronized (lock){
    flag = true;
    // 通知wait线程,我已经改变了条件,你可以继续返回执行了(返回之后继续判断while)
    // 但是此时通知notify()操作并立即不会释放锁,而是要等当前线程释放锁
    // TODO 我准备好配件了,我需要通知全部的组装流水线A.....
    lock.notifyAll();
    System.out.println(Thread.currentThread() + " hold lock, notify waitThread and flag is "+flag);
   }
   // 模拟跟流水线B竞争
   synchronized (lock){
    System.out.println(Thread.currentThread() + " hold lock again");
   }
  }
 }
}

输出结果:

Thread[WaitThread,5,main] is waiting, flag is false
Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true
Thread[NotifyThread,5,main] hold lock again
Thread[WaitThread,5,main] is running, flag is true

其中第三条跟第四条顺序可能会反着来的,这就是因为lock锁可能被红字部分的synchronized代码块竞争获取(这样wait()方法可能获取不到lock锁,不会返回),也可能被waitThread获取从wait()方法返回。

Thread[WaitThread,5,main] is waiting, flag is false
Thread[NotifyThread,5,main] hold lock, notify waitThread and flag is true
Thread[WaitThread,5,main] is running, flag is true
Thread[NotifyThread,5,main] hold lock again

三、等待/通知模式的应用

1、Thread.join()中源码应用

Thread.join()作用:当线程A等待thread线程终止之后才从thread.join()返回, 每个线程终止的前提是前驱线程终止,每个线程等待前驱线程终止后,才从join方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。

Thread.join()源码中,使用while选好判断前驱线程是否活着,如果前驱线程还活着就一直wait等待,当然如果超时的话就直接返回。

public final synchronized void join(long millis)
  throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
      throw new IllegalArgumentException("timeout value is negative");
    }
    // 这里的while(){wait(millis)} 就是利用等待/通知中的等待模式,只不过加上了超时设置
    if (millis == 0) {
      // while循环,当线程还活着的时候就一直循环等待,直到线程终止
      while (isAlive()) {
        // wait等待
        wait(0);
      }
      // 条件满足时返回
    } else {
      while (isAlive()) {
        long delay = millis - now;
        if (delay <= 0) {
          break;
        }
        wait(delay);
        now = System.currentTimeMillis() - base;
      }
    }
  }

2、其它的应用

  线程池的本质是使用一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列中取出工作并执行。那么,在这里的等待/通知模式的应用就是:

  工作队列中线程job没有的话也就是工作队列为空的情况下,等待客户端放入工作队列线程任务,并通知工作线程继续从工作队列中获取线程执行。

  注:关于线程池的应用源码这里不做介绍,因为一时也讲不完(自己也还没有完全消化),先简单介绍下应用到的地方还有概念。

  补充:其实数据库的连接池也类似线程池这种工作流程,也会涉及等待/通知模式。

3、等待/通知范式

  介绍了那么多应用,这种模式应该有个统一的范式来套用。对的,必然是有的:

  对于等待者(也可以称之为消费者):

synchronized (对象lock) {
    while (条件不满足) {
      对象.wait();
    }
    // TODO 处理逻辑
  }

  对于通知者(也可以称之为生产者):

synchronized (对象lock) {
    while (条件满足) {
      改变条件
      对象.notify();
    }
  }

  注意:实际开发中最好采用的是超时等待/通知模式,在thread.join()源码方法中完美体现

四、wait()、notify()、notifyAll()使用前需要加锁的原因----防止线程即饥饿

(1)其实根据wait()注意事项也能明白,wait()是释放锁的,那么不加锁哪来释放锁!

(2)wait()与notify()或者notifyAll()必须是搭配一起使用的,否则线程调用object.wait()之后,没有超时机制,也没有调用notify()或者notifyAll()唤醒的话,就一直处于WAITING状态,造成调用wait()的线程一直都是饥饿状态。

(3)由于第2条的,我们已知:即便我们使用了notify()或者notifyAll()去唤醒线程,但是没有在适当的时机唤醒(比如调用wait()之前就唤醒了),那么仍然调用wait()线程处于WAITING状态,所以我们必须保证wait()方法要么不执行,要么就执行完在被唤醒。也就是下列代码中1那里不能允许插入调用notify/notifyAll,自然而然就增加synchronized关键字,保证wait()操作整体执行不被破坏!

synchronized (对象lock) {
    while (条件不满足) {
      // 1 这里如果先执行了notify/notifyAll方法,那么2执行之后,该线程就一直WAITING
      对象.wait(); // 2
    }
    // TODO 处理逻辑
  }

用图片展示执行顺序就是:

(4)注意synchronized代码块中,代码错误或者其它原因线程终止的话,没有执行到wait()方法的话,是会自动释放锁的,不必担心会死锁。

到此这篇关于Java多线程中wait/notify通信模式的文章就介绍到这了,更多相关Java多线程wait/notify通信模式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java多线程中wait、notify、notifyAll使用详解

    基础知识 首先我们需要知道,这几个都是Object对象的方法.换言之,Java中所有的对象都有这些方法. public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait() throws InterruptedExceptio

  • 浅谈java多线程wait,notify

    前言 1.因为涉及到对象锁,Wait.Notify一定要在synchronized里面进行使用. 2.Wait必须暂定当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行 3.notify/notifyall: 唤醒线程 共享变量 public class ShareEntity { private String name; // 线程通讯标识 private Boolean flag = false; public ShareEntity() { } public String getN

  • Java多线程通讯之wait,notify的区别详解

    下面通过代码给大家介绍java多线程通讯之wait notify的区别,具体内容如下所示: class Res{ public String username; public String sex; } class Out extends Thread{ Res res; public Out(Res res){ this.res=res; } @Override public void run() { //写操作 int count=0; while (true){ // synchroniz

  • Java多线程通信wait()和notify()代码实例

    1.wait()方法和sleep()方法: wait()方法在等待中释放锁:sleep()在等待的时候不会释放锁,抱着锁睡眠. 2.notify(): 随机唤醒一个线程,将等待队列中的一个等待线程从等待队列中移到同步队列中. 代码如下 public class Demo_Print { public static void main(String[] args) { Print p = new Print(); new Thread() { public void run() { while (

  • java多线程之wait(),notify(),notifyAll()的详解分析

    wait(),notify(),notifyAll()不属于Thread类,而是属于Object基础类,也就是说每个对象都有wait(),notify(),notifyAll()的功能.因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了. wait导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或被其他线程中断.wait只能由持有对像锁的线程来调用. notify唤醒在此对象监视器上等待的单个线程.如果所有线程都在此对象上等

  • Java多线程基础 线程的等待与唤醒(wait、notify、notifyAll)

    本篇我们来研究一下 wait() notify() notifyAll() . DEMO1: wait() 与 notify() public class Test { static class ThreadOne extends Thread { private Callback mCallback; @Override public void run() { work(); if (mCallback != null) { mCallback.onResult(false); } } //

  • Java多线程中的wait/notify通信模式实例详解

    前言 最近在看一些JUC下的源码,更加意识到想要学好Java多线程,基础是关键,比如想要学好ReentranLock源码,就得掌握好AQS源码,而AQS源码中又有很多Java多线程经典的一些应用:再比如看了线程池的核心源码实现,又学到了很多核心实现,其实这些都可以提出来慢慢消化并变成自己的知识点,今天这个Java等待/通知模式其实是Thread.join()实现的关键,还有线程池工作线程中线程跟线程之间的通信的核心所在,故在此为了加深理解,做此记录! 参考资料<Java并发编程艺术>(电子PD

  • vuejs中父子组件之间通信方法实例详解

    本文实例讲述了vuejs中父子组件之间通信方法.分享给大家供大家参考,具体如下: 一.父组件向子组件传递消息 // Parent.vue <template> <div class="parent"> <v-child :msg="message"></v-child> </div> </template> <script> import VChild from './child.v

  • Java多线程中线程间的通信实例详解

    Java多线程中线程间的通信 一.使用while方式来实现线程之间的通信 package com.ietree.multithread.sync; import java.util.ArrayList; import java.util.List; public class MyList { private volatile static List list = new ArrayList(); public void add() { list.add("apple"); } publ

  • Java 中组合模型之对象结构模式的详解

    Java 中组合模型之对象结构模式的详解 一.意图 将对象组合成树形结构以表示"部分-整体"的层次结构.Composite使得用户对单个对象和组合对象的使用具有一致性. 二.适用性 你想表示对象的部分-整体层次结构 你希望用户忽略组合对象与单个对象的不同,用户将统一使用组合结构中的所有对象. 三.结构 四.代码 public abstract class Component { protected String name; //节点名 public Component(String n

  • java多线程中执行多个程序的实例分析

    我们知道多线程因为同时处理子线程的能力,对于程序运行来说,能够达到很高的效率.不过很多人对于多线程的执行方法还没有尝试过,本篇我们将为大家介绍创建线程的方法,在这个基础上,对程序执行多条命令的方法进行展示.下面我们就来看看具体的操作步骤吧. 1.创建线程对象我们需要用到Thread类,该类是java.lang包下的一个类,所以调用时不需要导入包.下面我们先创建一个新的子类来继承Thread类,然后通过重写run()方法(将需要同时进行的任务写进run()方法内),来达到让程序同时做多件事情的目的

  • java 迭代器模式实例详解

    java 迭代器模式实例详解 今天来818设计模式中的迭代器模式,也是java中Stack,List,Set等接口以及数组这个数据结构都会使用的一种模式. 首先,为什么使用迭代器模式,目的就是通过一个通用的迭代方法,隐藏stack,list,set以及数组中不同的遍历细节.也就是说,我不想让那些调用我的遍历容器的方法的人知道我到底是怎么一个一个的获取这些元素的(stack的pop,list的get,数组的array[i]),我只想让他知道他能 通过一个迭代器Iterator或者通过一个for e

  • Java后端学习精华之TCP通信传输协议详解

    目录 Socket连接模型 消息协议 传输过程中数据类型需要了解的细节 TCP通信代码 上篇教程回顾 ServerSocket --监听客户端的连接,他的作用主要是建立一个连接 -ServerSocket -建立连接,拿到一个Socket -Telnet 127.0.0.1 8888- 客户端使用Telnet访问服务端 建立连接 -服务端可以拿到一个Socket的对象 -获取这个对象的输入输出流 -写入和读取数据 Socket连接模型 服务端和客户端通过Socket进行连接,虽然是一个Socke

  • java编程创建型设计模式工厂方法模式示例详解

    目录 1.什么是工厂方法模式? 2.案例实现 3.JDK中的工厂方法模式 1.什么是工厂方法模式? 工厂方法模式设计方案:  将披萨项目的实例化功能抽象成抽象方法,在不同的口味点餐子类中具体实现. 工厂方法模式:  定义了一个创建对象的抽象方法,由子类决定要实例化的类.工厂方法模式将对象的实例化推迟到子类. 何时使用?  不同条件下创建不用实例时.方法是让子类实现工厂接口. 2.案例实现 假如说,我们现在有这样一个需求:客户在点披萨时,可以点不同口味的披萨,比如北京的奶酪pizza.北京的胡椒p

  • Java结构型设计模式之享元模式示例详解

    目录 享元模式 概述 目的 应用场景 优缺点 主要角色 享元模式结构 内部状态和外部状态 享元模式的基本使用 创建抽象享元角色 创建具体享元角色 创建享元工厂 客户端调用 总结 享元模式实现数据库连接池 创建数据库连接池 使用数据库连接池 享元模式 概述 享元模式(Flyweight Pattern)又称为轻量级模式,是对象池的一种实现.属于结构型模式. 类似于线程池,线程池可以避免不停的创建和销毁多个对象,消耗性能.享元模式提供了减少对象数量从而改善应用所需的对象结构的方式. 享元模式尝试重用

随机推荐