zoukankan      html  css  js  c++  java
  • 【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
    ", ch);
    		System.out.printf("容器中现在共有%d个字符!
    
    ", 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
    ", ch);
    		System.out.printf("容器中现在共有%d个字符!
    
    ", 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块,释放对象锁后,其余线程才可获得锁执行。

  • 相关阅读:
    (转)C# DllImport的用法
    (转)C#网络编程(异步传输字符串) Part.3
    (转)C#网络编程(订立协议和发送文件) Part.4
    C# tostring()汇总
    (转)C#网络编程(基本概念和操作) Part.1
    (转)关于数据库存储过程分页DatagridView BindingNavigator 控件的详细实现
    C# sql server 数据库备份和还原
    (转)C#网络编程(接收文件) Part.5
    2010年5月学习计划
    APUE学习笔记 Chapter 2 . Unix Standardization and Implementations
  • 原文地址:https://www.cnblogs.com/hainange/p/6334023.html
Copyright © 2011-2022 走看看