欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-12-04 14:58:28
基础知识 首先我们需要知道,这几个都是object对象的方法。换言之,java中所有的对象都有这些方法。 public final native void...

基础知识

首先我们需要知道,这几个都是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 interruptedexception {
  wait(0);
}

其中notify()、notifyall()、wait(long timeout)是本地方法

其次我们需要知道这几个方法主要是用来个线程之间通信的。那可能就有人会问,既然是用来线程之间通信的,那为什么这几个方法不是在线程类thread上呢?对于这个问题,我们先来看下这几个方法的具体使用方式再来回答这个问题。

用法

java中规定,在调用者三个方法时,当前线程必须获得对象锁。因此就得配合synchronized关键字来使用

//使用模式,不代表可运行代码
synchronized(object) {
  while(contidion) {
    object.wait();
  }
  //object.notify();
  //object.notifyall();
}

或者

public synchronized void methodname() {
  while(contidion) {
    object.wait();
  }
  //object.notify();
  //object.notifyall();
}

在synchronized拿到对象锁之后,synchronized代码块或者方法中,必定是会持有对象锁的,因此就可以使用wait()或者notify()。

通过上述使用方法,我们也能很好理解为什么这几个方法是在object上而不是在thread上。因为每个对象都可以作为synchronized锁的对象,因此wait、notify等必须和对象关联才能配合synchronized使用。

作用

方法 作用
wait 线程自动释放占有的对象锁,并等待notify。
notify 随机唤醒一个正在wait当前对象的线程,并让被唤醒的线程拿到对象锁
notifyall 唤醒所有正在wait当前对象的线程,但是被唤醒的线程会再次去竞争对象锁。因为一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。

1、wait()、notify/notifyall() 方法是object的本地final方法,无法被重写。

2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyall() 方法。

3、 由于 wait()、notify/notifyall() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。

当线程执行wait()方法时候,会释放当前的锁,然后让出cpu,进入等待状态。

只有当 notify/notifyall() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。

也就是说,notify/notifyall() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyall() 后立即退出临界区,以唤醒其他线程

4、wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。

5、notify 和wait 的顺序不能错,如果a线程先执行notify方法,b线程在执行wait方法,那么b线程是无法被唤醒的。

6、notify 和 notifyall的区别

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyall 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyall 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

7、在多线程中要测试某个条件的变化,使用if 还是while?

要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。如下代码:

public class k {
  //状态锁
  private object lock;
  //条件变量
  private int now,need;
  public void produce(int num){
    //同步
    synchronized (lock){
      //当前有的不满足需要,进行等待
      while(now < need){
        try {
          //等待阻塞
          wait();
        } catch (interruptedexception e) {
          e.printstacktrace();
        }
        system.out.println("我被唤醒了!");
      }
      // 做其他的事情
    }
  }
}

显然,只有当前值满足需要值的时候,线程才可以往下执行,所以,必须使用while 循环阻塞。注意,wait() 当被唤醒时候,只是让while循环继续往下走.如果此处用if的话,意味着if继续往下走,会跳出if语句块。但是,notifyall 只是负责唤醒线程,并不保证条件云云,所以需要手动来保证程序的逻辑。

实际案例

接下来我们就使用wait()、notify()来实现一个生产者、消费者模式。这个也是面试过程中可能会被问到的地方。至于什么是生产者消费者模式,不明白的同学请自行百度。

首先是一些基础的代码

private static boolean run = true;//控制是否生产和消费
private static final integer max_capacity = 5;//缓冲区最大数量
private static final linkedblockingqueue<string> queue = new linkedblockingqueue<>();//缓冲队列

生产者代码

/**
 * 生产者
 */
class producter extends thread {
  @override
  public void run() {
    while (run) {
      synchronized (queue) {
        while (queue.size() >= max_capacity * 2) {
          try {
            system.out.println("缓冲队列已满,等待消费");
            queue.wait();
          } catch (interruptedexception e) {
            e.printstacktrace();
          }
        }

        try {
          string string = uuid.randomuuid().tostring();
          queue.put(string);
          system.out.println("生产:" + string);
          thread.sleep(500);
        } catch (interruptedexception e) {
          e.printstacktrace();
        }

        queue.notifyall();//通知生产者和消费者
      }
    }
  }
}

消费者代码

/**
 * 消费者
 */
class consumer extends thread {
  @override
  public void run() {
    while (run) {
      synchronized (queue) {
        while (queue.isempty()) {
          try {
            system.out.println("队列为空,等待生产");
            queue.wait();
          } catch (interruptedexception e) {
            e.printstacktrace();
          }
        }

        try {
          system.out.println("消费:" + queue.take());
          thread.sleep(500);
        } catch (interruptedexception e) {
          e.printstacktrace();
        }

        queue.notifyall();//通知生产者和消费者
      }
    }
  }
}

代码说明

1、生产者和消费者都继承了线程thread,因为wait、notify本身就是线程间通信使用

2、生产者和消费者都有两层while,外层的while是用来判断是否运行生产者和消费者。内存的while用来判断队列queue是否已满或者为空,如果满足条件,则使得当前线程变成等待状态(等待notify)。

3、内层的条件判断为什么用while不用if,原因是当线程被wait之后,会释放对象锁。当等待的线程被notify之后,必须再次尝试去获取对象锁,如果没有获取到对象锁,那还必须等待,直到拿到对象锁之后才能向后执行。

4、当生产者生产了一个数据或者消费者消费了一个数据之后,使用notifyall()方法来通知所有等待当前对象锁的线程,但是一次只会有一个等待的线程能拿到锁。

5、我们使用queue作为锁的对象在不同线程之间进行通信

代码运行结果

队列为空,等待生产
生产:e422484e-8eb3-4a91-8a7c-97e21d7ef498
生产:7894b802-2529-4798-ba98-9f0657f39240
生产:848f6759-a427-4a94-89dc-3f484daaa467
生产:f711d3dc-972c-4c44-8640-faffe376d354
生产:38a08e62-d774-4ed5-8b51-f1ad534c246f
消费:e422484e-8eb3-4a91-8a7c-97e21d7ef498
消费:7894b802-2529-4798-ba98-9f0657f39240
消费:848f6759-a427-4a94-89dc-3f484daaa467
消费:f711d3dc-972c-4c44-8640-faffe376d354
消费:38a08e62-d774-4ed5-8b51-f1ad534c246f
队列为空,等待生产
生产:9fae26f3-0b6e-4fbd-9620-040667efe0af
生产:95bb1d88-e08a-4f70-a270-f75760994184
消费:9fae26f3-0b6e-4fbd-9620-040667efe0af
消费:95bb1d88-e08a-4f70-a270-f75760994184
队列为空,等待生产
生产:13d304bc-dff3-44a4-9527-2e0facd884e7
生产:2693e069-bae1-4beb-adcd-a3c3bf5d232b
消费:13d304bc-dff3-44a4-9527-2e0facd884e7
消费:2693e069-bae1-4beb-adcd-a3c3bf5d232b

由结果也可以看出来,生产和消费是无规则的,因为虽然notifyall()通知了所有的等待线程,但是不确定那个线程中能拿到对象锁。但是也有一个很明显的问题,因为同时只有一个线程能拿到对象锁,生产者和消费者不可能同时运行。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。