關於線程,博主寫過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塊,釋放對象鎖後,其餘線程纔可獲得鎖執行。