【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塊,釋放對象鎖後,其餘線程纔可獲得鎖執行。

發佈了98 篇原創文章 · 獲贊 330 · 訪問量 118萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章