【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块,释放对象锁后,其余线程才可获得锁执行。

posted on 2016-04-26 15:06  海南一哥  阅读(145)  评论(0编辑  收藏  举报

导航