Java線程學習筆記之線程協作(通信)

線程協作

在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在一個線程釋放掉該鎖後,處於準備就緒隊列中的線程會競爭獲得該對象鎖,而未獲得的線程仍然處於該隊列中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章