【java线程系列】java线程系列之线程间的交互wait()/notify()/notifyAll()及生产者与消费者模型

时间:2021-06-22 17:22:55

关于线程,博主写过java线程详解基本上把java线程的基础知识都讲解到位了,但是那还远远不够,多线程的存在就是为了让多个线程去协作来完成某一具体任务,比如生产者与消费者模型,因此了解线程间的协作是非常重要的,本博客主要讲解多个线程之间使用wait()/notify()/notifyAll()来进行交互的场景。

一wait()/notify()/notifyAll():

首先我们来看一下它们的函数定义:

 /* Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object.
* The current thread must own this object's monitor. The thread
* releases ownership of this monitor and waits until another thread
* notifies threads waiting on this object's monitor to wake up
* This method should only be called by a thread that is the owner
* of this object's monitor.
*/
public final void wait() throws InterruptedException {
wait(0);
} public final native void wait(long timeout) throws InterruptedException; /**
* Wakes up a single thread that is waiting on this object's
* monitor.If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the {@code wait} methods.
*/ public final native void notify(); /* Wakes up all threads that are waiting on this object's monitor. A
* thread waits on an object's monitor by calling one of the
* {@code wait} methods.
*/
public final native void notifyAll();

首先可以看到wait()/notify()/notifyAll()都是Object类中的方法,其次可以看到notify()/notifyAll(),与带一个参数的 wait(long timeout) 都被final native修饰的,即它们是本地方法且不允许被重写的。

另外从注释上我们可以获得以下信息:

1 调用某个对象的wait()方法能让当前线程阻塞,这个方法只能在拥有此对象的monitor的线程中调用(This method should only be called by a thread that is the owner of this     object's monitor.)。

2 调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程(Wakes up a single        thread that is waiting on this object's monitor.If any threads are waiting on this object, one of them is chosen to be awakened. )

3 调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程(Wakes up all threads that are waiting on this object's monitor.)

那么你可能会问为何这三个方法不是位于Thread类中而是在Object类中呢?这是因为这三个函数的操作都是与锁机制相关的,而在java中每个对象都对应一个对象锁,所以当某个线程等待某个对象的锁时,应该等待该对象来释放它自己的锁,即应该通过对象的方式来释放锁,所以将这些与锁相关的函数放在Object类中,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。

下面我们来看一下这几个函数之间的协作使用的代码示范:

public class Main {
publicstaticvoid main(String[] args) {
ThreadOne one = new ThreadOne();
one.start(); //启动一个子线程 synchronized (one) {//synchronized (one) 代表当前线程拥有one对象的锁,线程为了调用wait()或notify()方法,该线程必须是那个对象锁的拥有者
try {
System.out.println("等待对象one完成计算。。。");
one.wait(); //调用wait()方法的线程必须拥有对象one的锁,即必须在synchronized同步块中调用wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one对象计算的总和是:" + one.total);
}
}
} public class ThreadOne extends Thread {
int total; publicvoid run() {
synchronized (this) {
for (int i = 0; i < 7; i++) {
total += i;
} notify(); //完成计算后唤醒在此对象锁上等待的单个线程,在本例中主线程Main被唤醒
}
}
}

在该示例中定义了一个子线程ThreadOne,所以该程序存在两个线程,一个是默认的主线程Main,一个是自定义的ThreadOne,在自定义的线程中计算求和,在主线程中打印出计算结果,在Main线程中先启动子线程,然后调用wait()让主线程等待子线程运行,在子线程中计算完成后调用notify()唤醒Main线程打印出子线程中计算的结果。这样就做到了两个线程之间的交互。

二使用多线程模拟生产者与消费者模型:

生产者与消费者模型描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该模型的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

解决此模型的关键是让生产者在缓冲区满时交出对临界区的占用权,自己进入等待状态,等待消费者消费产品,等消费者消费了一定量产品后再唤醒生产者生产产品。

同样,当缓冲区为空时消费者也必须等待,等待生产者生产产品,等生产者生产了一定量产品后再唤醒消费者消费产品。

代码表示如下:

public class ProducerConsumer
{
public static void main(String[] args)
{
SyncStack ss = new SyncStack();
Producer p = new Producer(ss);
Consumer c = new Consumer(ss); Thread t1 = new Thread(p);
Thread t2 = new Thread(c); t1.start();
t2.start();
}
} class SyncStack
{
int cnt = 0;
char[] data = new char[6]; public synchronized void push(char ch)
{
while (cnt == data.length)
{
try
{
this.wait(); //wait是Object 类中的方法,不是Thread中的方法,Thread中wait也是继承自Object,
//this.wait();不是让当前对象wait,而是让当前锁定this对象的线程wait,同时释放对this的锁定。
//注意:如果该对象没有被锁定,则调用wait方法就会报错!即只有在同步方法或者同步代码块中才可以调用wait方法,notify同理
}
catch (Exception e)
{
}
}
this.notify(); //如果注释掉了本语句,可能会导致消费线程陷入阻塞(如果消费线程本身执行很慢的话,则消费线程永远不会wait,即永远不会阻塞),因为消费线程陷入阻塞, 所以生产线程因此不停生产产品达到6个后也陷入阻塞,最后显示的肯定是“容器中现在共有6个字符!”
//this.notify();叫醒一个现在正在wait this对象的一个线程,如果有多个线程正在wait this对象,通常是叫醒最先wait this对象的线程,但具体是叫醒哪一个,这是由系统调度器控制,程序员无法控制
// nority 和 notifyAll 都是Object 类中的方法 data[cnt] = ch;
cnt++; System.out.printf("生产了: %c\n", ch);
System.out.printf("容器中现在共有%d个字符!\n\n", cnt);
} public synchronized char pop()
{
char ch; while (0 == cnt)
{
try
{
this.wait();
}
catch (Exception e)
{
}
}
this.notify(); //如果注释掉了本语句,可能会导致生产线程陷入阻塞(如果生产线程本身执行很慢的话,则生产线程永远不会wait,即永远不会阻塞),因为生产线程陷入阻塞,消费线程因此不停取出产品,当容器中再也没有产品时消费线程也陷入阻塞,最后显示的肯定是“容器中现在共有0个字符!” ch = data[cnt-1];
--cnt; System.out.printf("取出: %c\n", ch);
System.out.printf("容器中现在共有%d个字符!\n\n", cnt); return ch;
}
} class Producer implements Runnable
{
SyncStack ss = null; public Producer(SyncStack ss)
{
this.ss = ss;
} public void run()
{
char ch; //总共生产20个产品
for (int i=0; i<20; ++i)
{
ch = (char)('a'+i);
ss.push(ch);
// try
// {
// Thread.sleep(500);
// }
// catch (Exception e)
// {
// }
}
}
} class Consumer implements Runnable
{
SyncStack ss = null; public Consumer(SyncStack ss)
{
this.ss = ss;
} //总共消费20个产品
public void run()
{
for (int i=0; i<20; ++i)
{
ss.pop();
try
{
Thread.sleep(500);
}
catch (Exception e)
{
}
}
}
}

三总结:

1  如果调用某个对象的wait()方法,当前线程必须拥有这个对象的monitor(即锁),因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。

2调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor,然后进入等待状态,等待后续再次获得此对象的锁,而Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它不会释放对象锁;

3 notify()方法能够唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象的monitor的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。

调用某个对象的notify()方法,当前线程也必须拥有这个对象的monitor,因此调用notify()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法),nofityAll()方法能够唤醒所有正在等待该对象的monitor的线程

4一个线程被唤醒不代表它能立即获得对象的monitor,当且仅当调用完notify()或者notifyAll()且退出synchronized块,释放对象锁后,其余线程才可获得锁执行。