線程協作
在Java線程的使用中,僅僅有線程同步是不夠的,還需要線程與線程協作(即通信),生產者/消費者問題是一個經典的線程同步以及通信的案例。下面我們通過他來理解線程協作。
該問題描述了兩個共享固定大小緩衝區的線程,即所謂的“生產者”和“消費者”在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。要解決該問題,就必須讓生產者在緩衝區滿時進入等待(要麼乾脆就放棄數據),等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區添加數據。同樣,也可以讓消費者在緩衝區空時進入等待,等到生產者往緩衝區添加數據之後,再喚醒消費者,通常採用線程間通信的方法解決該問題。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入等待,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。下面是JDK5之前傳統線程的通信方式。問題代碼如下:
package thread_test;
import java.util.ArrayList;
import java.util.List;
/**定義一個工作區類*/
public class WorkArea {
List<Object> dataBuffer = new ArrayList<Object>();//裝數據的共享緩存區
/**
* 取數據
*/
public synchronized Object getData(){
while(dataBuffer.size() == 0){
try{
System.out.println("消費者線程: "+Thread.currentThread().getName()+" 未檢查到數據,進入等待...");
wait();
System.out.println("消費者線程: "+Thread.currentThread().getName()+" 被喚醒並獲得鎖,繼續執行...");
}catch(InterruptedException e){
e.printStackTrace();
}
}
Object data = dataBuffer.get(0);
dataBuffer.clear();//清空緩存區S
System.out.println("消費者線程: "+Thread.currentThread().getName()+"拿到數據,釋放鎖,結束運行");
notify();//喚醒阻塞隊列的某線程到就緒隊列
return data;
}
/**
* 寫入數據
*/
public synchronized void putData(Object data){
while(dataBuffer.size() > 0){
try{
System.out.println("生產者線程: "+Thread.currentThread().getName()+" 檢查到數據,進入等待...");
wait();
System.out.println("生產者線程: "+Thread.currentThread().getName()+" 被喚醒並獲得鎖,繼續執行...");
}catch(InterruptedException e){
e.printStackTrace();
}
}
dataBuffer.add(data);
System.out.println("生產者線程: "+Thread.currentThread().getName()+"寫入數據,釋放鎖,結束運行");
notify();//喚醒阻塞隊列的某個線程到就緒隊列
}
/**
* 生產者線程
*/
static class Producer implements Runnable{
private WorkArea workArea;
private Object data = new Object();
public Producer(WorkArea workArea) {
this.workArea = workArea;
}
@Override
public void run() {
workArea.putData(data);
}
}
/**
* 消費者線程
*/
static class Customer implements Runnable{
private WorkArea workArea;
public Customer(WorkArea workArea) {
this.workArea = workArea;
}
@Override
public void run() {
workArea.getData();
}
}
public static void main(String[] args){
WorkArea workArea = new WorkArea();
for(int i=1;i<=3;i++){
new Thread(new Customer(workArea)).start();
new Thread(new Producer(workArea)).start();
}
}
}
輸出結果:
消費者線程: Thread-0 未檢查到數據,進入等待...
生產者線程: Thread-1寫入數據,釋放鎖,結束運行
消費者線程: Thread-0 被喚醒並獲得鎖,繼續執行...
消費者線程: Thread-0拿到數據,釋放鎖,結束運行
消費者線程: Thread-2 未檢查到數據,進入等待...
消費者線程: Thread-4 未檢查到數據,進入等待...
生產者線程: Thread-3寫入數據,釋放鎖,結束運行
消費者線程: Thread-2 被喚醒並獲得鎖,繼續執行...
消費者線程: Thread-2拿到數據,釋放鎖,結束運行
消費者線程: Thread-4 被喚醒並獲得鎖,繼續執行...
消費者線程: Thread-4 未檢查到數據,進入等待...
生產者線程: Thread-5寫入數據,釋放鎖,結束運行
消費者線程: Thread-4 被喚醒並獲得鎖,繼續執行...
消費者線程: Thread-4拿到數據,釋放鎖,結束運行
wait()、notify() 和 notifyAll() 方法
Object 類定義了 wait()、notify() 和 notifyAll() 方法。要執行這些方法,必須擁有相關對象的鎖。
Wait() 會讓調用線程休眠,直到用 Thread.interrupt() 中斷它、過了指定的時間、或者另一個線程用 notify() 或 notifyAll() 喚醒它。
當對某個對象調用 notify() 時,如果有任何線程正在通過 wait() 等待該對象,那麼就會喚醒其中一個線程T,從對象的等待集中刪除線程 T,並重新進行線程調度。然後,該線程以常規方式與其他線程競爭,以獲得在該對象上同步的權利(即獲得該對象的鎖)。當對某個對象調用 notifyAll() 時,會喚醒所有正在等待該對象的線程。
在上面的代碼中,調用對象的wait()方法時,會放在while循環中,而不是if循環,這是因爲在沒有被通知、中斷或超時的情況下,線程還可以喚醒一個所謂的虛假喚醒 (spurious wakeup)。雖然這種情況在實踐中很少發生,但是應用程序必須通過以下方式防止其發生,即對應該導致該線程被提醒的條件進行測試,如果不滿足該條件,則繼續等待。也就是把它放在while循環中。還有,wait和notify方法必須工作於synchronized內部,且這兩個方法只能由鎖對象來調用。因爲調用該方法的線程必須擁有此對象監視器
sleep()和wait()的區別
對於sleep()方法,我們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。
sleep()方法導致了程序暫停執行指定的時間,讓出cpu該其他線程,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態。
在調用sleep()方法的過程中,線程不會釋放對象鎖。
而當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備,在前一個線程釋放鎖後,準備就緒隊列中的線程開始競爭,獲取對象鎖進入運行狀態。
在等待鎖定池中的線程,如果不是時間到了,自動喚醒或者被notify喚醒,那麼會永遠處於等待狀態(你可以把上面代碼中的兩個notify()註釋掉,運行代碼,就會發現凡是進入等待狀態的線程都沒有被喚醒繼續執行任務,也不會打印出’結束運行’字段)。而synchronized在一個線程釋放掉該鎖後,處於準備就緒隊列中的線程會競爭獲得該對象鎖,而未獲得的線程仍然處於該隊列中。