【java线程系列】java线程系列之线程间的交互wait()/notify()/notifyAll()及生产者与消费者模型
关于线程,博主写过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块,释放对象锁后,其余线程才可获得锁执行。