高級程序員需知的併發編程知識(一)

併發編程簡介

併發編程式Java語言的重要特性之一,固然也是最難以掌握的內容。編寫可靠的併發程序是一項不小的挑戰。可是,做爲程序員的咱們,要變得更有價值,就須要啃一些硬骨頭了。所以,理解併發編程的基礎理論和編程實踐,讓本身變得更值錢吧。java

使用併發編程的優點

一、充分利用多核CPU的處理能力

如今,多核CPU已經很是廣泛了,普通的家用PC基本都雙核、四核的,況且企業用的服務器了。若是程序中只有一個線程在運行,則最多也只能利用一個CPU資源啊,若是是一個四核的系統,豈不是最多隻利用了25%的CPU資源嗎?嚴重的浪費啊!程序員

另外若是存在I/O操做的話,單線程的程序在I/O完成以前只能等着了,處理器完成處於空閒狀態,這樣能處理的請求數量就很低了。換成多線程就不同了,一個線程在I/O的時候,另外一個線程能夠繼續運行,處理請求啊,這樣,吞吐量就上來了。面試

二、方便業務建模

若是在程序中只包含一種類型的任務,那麼比包含多種不一樣類型的任務的程序要更易於編寫、錯誤更少,也更容易測試。若是在業務建模中,有多種類型的任務場景。咱們可使用多線程來解決,讓每一個線程專門負責一種類型的任務。數據庫

經過使用線程,能夠將負責而且一步的工做流進一步分解爲一組簡單而且同步的工做流,每一個工做流在一個單獨的線程中運行,並在特定的同步位置進行交互。編程

併發編程帶來的風險

雖然併發編程幫助咱們提升了程序的性能,同時也提升對咱們程序員的要求,由於在編寫併發程序的過程當中,一不當心就面臨着多線程帶來的風險。這些風險主要是安全性問題、活躍性問題和性能問題。緩存

一、安全性問題

安全性問題多是很是複雜的,在多線程場景中,若是沒有正確地使用同步機制,會致使程序結果的不肯定性,這是很是危險的。安全

好比咱們熟知的 count++ 問題服務器

public class UnsafeCount {
    private static int count;
    
    public int getCount(){
        return count++;
    }
}

上面的代碼,在單線程環境中沒有問題。可是若是是多個線程同時訪問getCount方法,則不會獲得指望的正確結果。網絡

緣由在於count ++ 不是CPU級別的原子指令,咱們寫了一條語句,可是在底層實際上包含了三個獨立的操做:讀取count,將count加1,將計算結果再寫會主內存。而這多個線程有機會在其中任何一個操做時發生切換,這樣便有可能兩個線程拿到了一樣的值,讓後執行加1的操做。多線程

二、活躍性問題

當某個操做沒法繼續執行下去的時候,就會發生活躍性問題。在串行程序中,活躍性問題形式之一多是無心中形成的無限循環。在多線程場景中,若是有線程A在等待線程B釋放其持有的資源,而線程B永遠都不釋放該資源,那麼線程A將永遠地等待下去。

多線程中的活躍性問題通常指的就是死鎖、飢餓、活鎖等。

三、性能問題

原本是用多線程是爲了提升程序性能的,結果卻產生了性能問題。性能問題包括多個方面,例如服務時間過長,響應不靈敏,吞吐量過地、資源消耗太高等。

使用多線程而產生性能問題的根本緣由就是,建立線程、切換線程都是要帶來某種運行時開銷的。若是咱們的程序在頻繁的建立線程,那很快建立線程的消耗將增長,拖累程序總體性能。同時頻繁的線程切換,也會產生性能問題。

建立線程的幾種方法

在使用Java開始編寫併發程序時,咱們首先要知道在Java中應該如何建立線程,至少有下面的三種方法。經過線程池建立線程留到後面線程池章節單獨說明。

實現Runnable接口

咱們經過實現一個Runnable接口,將線程要執行的任務封裝起來。

public class MyTask implements Runnable{
    public void run() {
        // 要實行的任務
    }
}

使用Thread對象啓動線程

public class MyTaskThread {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyTask());
        thread.start();
    }
}

實現Callable接口

能夠看到實現Runnable接口啓動的線程是沒有返回值的。而Callable接口能夠實現有返回值地啓動線程。

public class MyCallableTask implements Callable<String> {
    public String call(){
        String str = "併發編程";
        return "hello" + str;
    }
}

經過FutureTask 咱們能夠獲取到返回值

public class MyCallableTaskThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallableTask callableTask = new MyCallableTask();
        FutureTask<String> futureTask = new FutureTask<String>(callableTask);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

繼承Thread類

繼承Thread類,重寫run方法。

public class MyThread extends Thread{
    @Override
    public void run() {

        // 執行任務
    }

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

通常不建議經過這種方式建立線程,由於:

  • Java 不支持多重繼承,所以繼承了 Thread 類就沒法繼承其它類,可是能夠實現多個接口;
  • 類可能只要求可執行就行,繼承整個 Thread 類開銷過大。

線程安全性問題

咱們編寫併發程序,最早要考慮的就是安全性問題,要保證在多線程執行條件下程序運行結果的正確性。

要編寫線程安全的代碼,其核心就是要對狀態訪問的操做進行管理,特別是對共享的和可變的狀態的訪問。

當多個線程訪問某個狀態變量而且其中有一個線程執行寫入操做時,必須採用同步機制來協同這些線程對變量的訪問。Java中的主要同步機制是關鍵字synchronized,它提供了一種獨佔的加鎖方式,可是「同步」這個術語還包括volatile類型的變量,顯示鎖(Lock)以及原子變量。

若是當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那麼程序就會出現錯誤。有三種方式能夠修復這個問題:

  • 不在線程之間共享該狀態變量
  • 將狀態變量修改成不可變的變量
  • 在訪問狀態變量時使用同步

原子性

這裏的原子性其實和數據庫事務中的原子性意義是相同的。咱們把不可分割的一組操做叫作原子操做,這種不能中斷的特性叫作原子性。

例如上面提到的 count++ 的問題,在java語法上,這看上去是一條指令,其實在CPU層面,至少須要三條CPU指令,所以,對CPU而言,count++的操做不是一個原子操做。

在這裏插入圖片描述

CPU能保證的原子操做是CPU指令級別的,而不是高級語言的一條語句。

競態條件

在併發編程中,由於線程切換,致使不恰當的執行時序而出現不正確結果的狀況,叫作競態條件。上面的count++ 的例子中就存在着競態條件。

最多見的競態條件類型就是「先檢查後執行」操做,即經過一個可能失效的觀測結果來決定下一步的動做。

所以在併發編程實踐中,要避免競態條件的發生,才能保證線程安全性。

加鎖機制

原子性問題的源頭是線程切換,而操做系統作線程切換是依賴CPU中斷的,因此禁止CPU中斷就可以禁止線程切換。可是禁止線程切換就能保證原子性嗎?

答案是並不能,例如在多核32位操做系統下,執行long類型變量的寫操做。由於long類型變量是64位,在32位CPU上執行寫操做會被拆分紅兩次寫操做(寫高32位和寫低32位)。

在這裏插入圖片描述

可能會出現,在同一時刻,一個線程A在CPU-1上執行寫高32位指令,另外一個線程B在CPU-2上也在執行寫高32位指令,這樣就會出現詭異的bug。此時,禁止CPU中斷並不能保證同一時刻只有一個線程執行。

在這裏插入圖片描述

所以,咱們要有一種機制,保證同一時刻只有一個線程執行。咱們稱之爲「互斥」。

簡易鎖模型

根據互斥特性,咱們能夠嘗試構建一種簡易的鎖模型。

在這裏插入圖片描述

經過加鎖的操做,使得同一時刻,只有一個線程在執行臨界區的代碼。

Java語言提供的內置鎖技術:sychronized

Java語言提供了關鍵字synchronized,就是一種鎖的實現。準確的來講,這種實現是JVM幫咱們實現的。

synchronized關鍵字能夠用來修飾方法,也能夠用來修飾代碼塊。基本的使用以下:

public class SyncDemo {
    
    // 修飾非靜態的方法
    synchronized void find(){
        // 臨界區代碼
    }
    
    // 修飾靜態方法
    synchronized static void wood(){
        // 臨界區代碼
    }
    
    // 修飾代碼塊
    Object lock = new Object();
    void save(){
        synchronized (lock){
            // 臨界區代碼
        }
    }
}

這裏有一個 類鎖和對象鎖的概念,好比上面修飾靜態方法的synchronized,是以SyncDemo.class 類爲鎖對象的,而修飾普通實例方法的synchronized,是以當前實例對象爲鎖對象的。

synchronized是一種內置鎖,線程在進入同步代碼塊以前會自動得到鎖,而且在退出同步代碼塊時自動釋放鎖。另外synchronized仍是一種互斥鎖,互斥意味着當線程A嘗試獲取一個由線程B持有的鎖時,線程A必須等待或者阻塞,直到線程B釋放這個鎖。若是線程B永遠不釋放鎖,那麼線程A也將永遠地等待下去。

內置鎖synchronized是能夠重(chong)入的。 可重入的意思是若是某個線程在嘗試獲取一個已經有它本身持有的鎖時,這個請求就會成功。

若是內置鎖不是可重入的,那下面的代碼將會發生死鎖。由於Payment和AliPayment的doService()方法都是synchronized的,所以每一個方法在執行前都會獲取Payment上的鎖,若是內置不是可重入的,那麼在執行super.doService()方法時,將沒法得到鎖,由於這個鎖已經被持有,從而AliPayment方法就不會結束,從而也不會釋放鎖,線程將永遠等待下去。

public class Payment {
    public synchronized void doService(){
        // .....
    }
}

public class AliPayment extends Payment{
    @Override
    public synchronized void doService() {
        System.out.println("使用支付寶支付");
        super.doService();
    }
}

基礎線程機制

線程的生命週期(6種狀態)

經過查看Thread源碼,咱們能夠知道線程總共有6中狀態。

在這裏插入圖片描述
一個線程只能處於一種狀態,線程在這幾種狀態之間的轉換便構成了線程的生命週期。

在這裏插入圖片描述

這張圖須要熟記於胸,面試高頻題。

sleep和join,yield

sleep是Thread類的一個靜態方法,它讓當前正在執行的線程睡眠指定的毫秒數。

public void run() {

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 執行任務
    }

須要注意的是sleep方法會拋出InterruptedException 中斷異常。

join方法:

一個線程能夠在其餘線程之上調用join方法,其做用是等待一段時間直到第二個線程運行結束才繼續執行。

經過看join的源碼能夠知道,join的底層實際上是在調用wait方法實現線程協做的。

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

yield方法:

是一種給線程調度器的暗示:讓當前線程讓出CPU的使用權,讓其餘線程執行一會。不過這種暗示沒有任何機制保證它將會被採納。

關於sleep、join、yield,能夠移步下面這篇進一步瞭解。
Java多線程中join、yield、sleep方法詳解

線程的優先級

經過setPriority方法,咱們能夠設置線程的優先級。線程的優先級將該線程的重要性傳遞給了調度器。儘管CPU處理現有線程集的順序是不肯定的,可是調度器將傾向於讓優先級高的線程先執行。然而,這並非意味着優先級低的線程將得不到執行。優先級低的線程僅僅是執行的頻率較低而已。

須要注意的是試圖經過控制線程的優先級來控制線程的執行順序,這是徹底錯誤的作法。

對象的共享

只有正確地共享和發佈對象,才能保證多線程同時訪問的安全性。

可見性問題(Volatile變量)

什麼是可見性問題。舉個例子,當讀操做和寫操做在不一樣的線程中執行時,咱們沒法保證執行讀操做的線程可以及時地看到寫線程寫入的值。這就是可見性問題。

下面的程序說明了當多個線程在沒有同步的狀況下共享數據會出現的問題。

public class NoVisibility {
    private static boolean ready;
    private  static int number;

    private static class ReaderThread extends Thread{
        public void run(){
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

在代碼中,主線程和讀線程都在訪問共享的變量ready和number。主線程啓動讀線程,而後將number設置爲42,並將ready設置爲true。讀線程則一直循環直到發現ready爲true時,而後輸出number的值。咱們指望是輸出42。但事實上有可能輸出0,或者程序根本沒法終止。這是由於代碼中沒有使用足夠的同步機制,沒法保證主線程修改的number和ready值對於讀線程來講是可見的。

重排序:

上面程序可能會輸出0,即讀線程看到了ready的值,但卻沒有看到以後寫入的number的值(代碼中倒是先寫入number,再寫入ready,順序變了),這種現象叫作「重排序」。

在沒有同步的狀況下,編譯器、處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整。

volatile變量:

volatile變量是java語言提供的一種稍弱的同步機制(比起synchronized鎖而言),用來確保將變量的更新操做通知到其餘線程。

當把變量申明成volatile類型後,編譯器與運行時都會注意到這個變量是共享的,所以不會將該變量上的操做與其餘內存操做一塊兒重排序。volatile變量不會被緩存在寄存器或者對其餘處理器不可見的地方,所以在讀取volatile類型的變量時總會返回最新寫入的值。

在訪問volatile類型的變量時不會執行加鎖的操做,所以也就不會使執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。

加鎖機制既能夠保證可見性又能夠保證原子性,可是volatile變量只能確保可見性。

當且僅當知足一下全部條件時,才應該使用volatile變量:

  • 對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
  • 該變量不會與其餘狀態變量一塊兒歸入不變形條件中。
  • 在訪問變量時不須要加鎖。

發佈和逸出

「發佈」一個對象的意思是指,使對象可以在當前做用域以外的代碼中使用。例如,將一個指向該對象的引用保存到其餘代碼能夠訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其餘類的方法中。

在許多狀況下,咱們須要確保對象及其內容狀態不被髮布。而在某些狀況下,咱們又須要發佈某個對象,但若是在發佈時要確保線程安全性,則可能須要同步。

當某個不該該發佈的對象被髮布時,這種狀況就被稱爲逸出。

下面是一個發佈的例子:

public class PublishObject {
    public static List<NoVisibility> list;
    
    // init方法實例化的list對象被保存在了公有的靜態變量中
    public void init(){
        list = new ArrayList<NoVisibility>();
    }
}

線程封閉

前面說到,當訪問共享的可變數據時,一般須要使用同步。一種避免使用同步的方式就是不共享數據。若是僅在但線程內訪問數據,就不須要使用同步。這種技術被稱爲線程封閉

當某個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即便被封閉的對象自己不會線程安全的。

那在具體的編程實踐中,該如何實現線程封閉呢,其實能夠經過局部變量或ThreadLocal類等。

不變性(Final域)

知足同步需求的另外一種方法是使用不可變對象。咱們目前爲止探討的全部原子性和可見性的問題,都和多線程訪問可變的狀態相關。若是這個對象自己的狀態不會發生任何改變,那這些複雜性都消失了。咱們也不須要同步機制了。

若是某個對象在被建立後其狀態就不能被修改,那麼這個對象就稱爲不可變對象。而不可變對象必定是線程安全的。

不可變性並不等於將對象中的全部域都聲明爲final的,即便都聲明爲final類型的,這個對象也仍然是可變的,由於在final域中能夠保存對可變對象的引用。

當知足如下條件時,對象纔是不可變的:

  • 對象建立之後其狀態就不能修改。
  • 對象的全部域都是final類型。
  • 對象是正確建立的(在對象的建立期間,this引用沒有逸出)。

final域

final域是不能被修改的(但若是final域引用的是可變對像,那麼這些被引用的對象是能夠修改的)。然而,在Java內存模型中,final域還有這特殊的語義。final域能確保初始化過程的安全性,從而能夠不受限制地訪問不可變對象,並在共享這些對象時無需同步。

一種好的編程習慣是,除非須要某個域是可變的,不然應將其聲明爲final域。

取消與關閉

通常來講,咱們啓動一個線程,而後等着它天然 運行結束就完了。可是可能存在這樣一種需求,咱們有時候想提早結束任務或者線程。好比用戶點了取消按鈕,須要快速關閉某個應用等。

取消某個操做的緣由可能有不少:

  • 用戶請求取消。好比點擊了圖形界面的取消按鈕。
  • 有時間限制的操做。當達到超時時間設置時必須取消正在進行的任務。
  • 由於產生錯誤了,須要取消正在進行的任務。

Java語言早期版本中可能存在Thread.stop和suspend等方法去終止一個線程,但這些方法由於安全性問題都已經被廢棄了。

咱們通常能想到的去終止一個線程的方法,多是去檢查一個volatile類型的boolean值,經過改變boolean值讓線程停下來。像下面這樣:

public class CancleRunnable implements Runnable{

    private static volatile boolean cancelled;
    public void run() {
        while(!cancelled){
            // 業務代碼
        }
    }
    public static void cancle(){cancelled = true;}

    public static void main(String[] args) {
        new Thread(new CancleRunnable()).start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            CancleRunnable.cancle();
        }
    }
}

若是任務中調用了一些阻塞方法,好比從磁盤或是網絡讀取字節流。則經過檢查標誌位的方式取消或者結束任務將變得不可行了,由於存在可能永遠不會檢查標誌位的狀況,這樣任務任務永遠不會結束了。

在Java線程中提供了一種中斷機制,可以使一個線程終止另外一個線程的當前工做。

中斷

線程中斷是一種協做機制,線程能夠經過這種機制來通知另外一個線程,告訴它在合適的或者可能的狀況下中止當前的工做,並轉而執行其餘的工做。

每一個線程都有一個boolean類型的中斷狀態。當中斷線程時,這個線程的中斷狀態將被設置爲true。

Thread類中包含了和線程中斷相關的3個方法:

public class Thread{
    // 中斷目標線程
    public void interrupt(){.....}

    // 返回目標線程的中斷狀態
    public boolean isInterrupted(){......}

    // 靜態方法,清楚當前線程的中斷狀態,並返回它以前的值
    public static boolean interrupted(){......}
}

阻塞方法,好比Thread.sleep和Object.wait()等,都會檢查線程什麼時候中斷,並在發現中斷時提早返回。它們在響應中斷時執行的操做包括:清理中斷狀態,拋出InterruptedException,表示阻塞操做因爲中斷而提早結束。所以,這些阻塞方法拋出InterruptedException就是提供給程序員一種讓程序中止的入口。

public class InterruptTask extends Thread {

    private final BlockingQueue<BigInteger> queue;

    public InterruptTask(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!Thread.currentThread().isInterrupted()) {
                queue.put(p = p.nextProbablePrime());
                System.out.println(queue.size());
            }
        } catch (InterruptedException e) {
            // 容許線程退出
            e.printStackTrace();
        }
    }

    public void cancel() {
        interrupt();
    }

    public static void main(String[] args) {
        Thread thread = new InterruptTask(new ArrayBlockingQueue(10000));
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            ((InterruptTask) thread).cancel();
        }

    }
}

JVM關閉

JVM能夠正常關閉,也能夠強行關閉。正常關閉的方式主要有:當最後一個「正常(非守護)」線程結束時,或者當調用了System.exit時,或者經過其餘特定平臺的方法關閉。強行關閉方式能夠是經過調用Runtime.halt或者在操做系統中「殺死」JVM進程。

關閉鉤子

在正常關閉中,JVM首先會調用全部已註冊的關閉鉤子(Shutdown Hook)。關閉鉤子是指經過Runtime.addShutdownHook註冊的但還沒有開始的線程。 JVM並不能保證關閉鉤子的調用順序。

在關閉應用程序線程時,若是有(守護或非守護)線程仍然在運行,那麼這些線程接下來將與關閉進程併發執行。當全部的關閉鉤子都執行結束時,若是runFinalizersOnExit爲true,那麼JVM將運行終結器,而後在中止。當被強行關閉時,只是關閉JVM,而不會運行關閉鉤子。

守護線程

有時候後你但願建立一個線程來執行一些輔助工做,可是又不但願這個線程阻礙JVM的正常關閉。這種狀況下可使用守護線程。

線程分爲兩種:普通線程和守護線程。JVM在啓動時建立的全部線程中,除了主線程之外,其餘的線程都是守護線程(好比垃圾回收線程)。咱們平時在代碼中建立的線程是普通線程,由於新建的線程會繼承建立它的線程的守護狀態。

普通線程和守護線程的主要區別在當線程退出的時候發生的操做。當JVM中止時,全部仍然存在的守護線程都將拋棄——既不會執行finally代碼塊,也不會執行回捲棧,而JVM只是直接退出。

線程協做

當使用多線程同時運行多個任務時,咱們經過加鎖(互斥鎖)的方式實現了多個任務的同步,解決了任務之間干涉問題,其本質是在解決安全性問題。線程協做則是在同步基礎上要多個任務之間有協調,也就是說有些任務之間是有前後執行順序的,一個任務結束了,或者一些任務準備好了,才能執行接下來的任務。

這種協做,首先是創建在互斥的基礎上的。這也就是爲何wait()和notify()方法必需要在同步代碼塊之中了。

wait和notify

當一個任務在方法裏遇到了對wait()的調用的時候,當前執行的線程將被掛起,對象上的鎖被釋放,由於wait()方法釋放了鎖,這就意味着另外一個任務能夠得到鎖,所以在該對象中的其餘synchronized方法能夠在wait()期間被調用。而其餘方法中通常會使用notify()或者notifyAll()來從新喚起等待的線程。

wait()有兩種形式,其中一種是帶毫秒參數的重載方法,含義和sleep()方法裏參數的意思相同,都是指「在此期間暫停」。但和sleep不一樣的是,對於wait()方法而言:

  • 在wait()期間對象鎖是釋放的。
  • 能夠經過notify、notifyAll或者時間到期了,從wait()中恢復執行。

這裏引用《Java編程思想》中的給汽車打蠟的例子來作說明。

WaxOMatic.java有兩個過程:一個是將蠟塗到Car上,一個是拋光它。塗蠟以前要先拋光。即拋光-->塗蠟 -->拋光-->塗蠟.....。這樣一個交替的過程。

public class Car {
    private boolean waxOn = false;

    public synchronized void waxed(){
        waxOn = true;
        notifyAll();
    }

    public synchronized void buffed(){
        waxOn = false;
        notifyAll();
    }

    public synchronized void waitForWaxing() throws InterruptedException {
        while (waxOn == false){
            wait();
        }
    }

    public synchronized void waitForBuffing() throws InterruptedException {
        while (waxOn == true){
            wait();
        }
    }
}

public class WaxOn implements Runnable{
    private Car car;

    public WaxOn(Car car) {
        this.car = car;
    }

    public void run() {
        try {
            while (!Thread.interrupted()){
                System.out.println("Wax on");
                TimeUnit.MICROSECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax On task");
    }
}

public class WaxOff implements Runnable{
    private Car car;

    public WaxOff(Car car) {
        this.car = car;
    }

    public void run() {
        try {
            while (!Thread.interrupted()){
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MICROSECONDS.sleep(200);
                car.buffed();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax Off task");
    }
}

public class WaxOMatic {
    public static void main(String[] args) throws Exception {
        Car car = new Car();
        ExecutorService exec = Executors.newFixedThreadPool(2);
        exec.execute(new WaxOff(car));
        exec.execute(new WaxOn(car));
        TimeUnit.MICROSECONDS.sleep(10);
        exec.shutdownNow();
    }
}

運行結果以下:

Wax on
Wax Off!
Wax on
Wax Off!
Wax on
Wax Off!
Wax on
Wax Off!
Wax on
Exiting via interrupt
Ending Wax On task
Wax Off!
Exiting via interrupt
Ending Wax Off task

notify和notifyAll的區別

可能有多個任務在單個Car對象上處於wait狀態,所以調用notifyAll()比只調用notify()要更安全。在使用notify()時,在衆多等待的線程中只能有一個被喚醒。若是全部這些任務在等待不一樣的條件,那麼你就不會知道是否喚醒了恰當的任務。所以應儘可能多地使用notifyAll,它老是沒錯的。

另外須要注意的是,當notifyAll()因某個特定鎖而被調用時,只有等待這個鎖的任務纔會被喚醒。

生產者-消費者模型

下面的例子是經過wait和notify協做機制實現的生產者-消費者模型。示例中生產者生產麪包,消費者消費麪包。

public class Breed {
    private final int orderNum;

    public Breed(int orderNum) {
        this.orderNum = orderNum;
    }

    @Override
    public String toString() {
        return "Bread: " + orderNum;
    }
}

public class Producer implements Runnable{
    private Dish dish;
    private int count;
    public Producer(Dish dish) {
        this.dish = dish;
    }

    public void run() {
        try {
            while (!Thread.interrupted()){
                synchronized (this){
                    while (dish.breed != null){
                        wait();// 盤子中有面包時等待被消費
                    }
                }

                synchronized (dish.consumer){
                    dish.breed = new Breed(count++); // 生產一個以後要喚醒消費者消費
                    System.out.println("生產了一個麪包" + dish.breed);
                    dish.consumer.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            System.out.println("Producer interrupted");;
        }
    }
}

public class Consumer implements Runnable{
    private Dish dish;

    public Consumer(Dish dish) {
        this.dish = dish;
    }

    public void run() {
        try {
            while (!Thread.interrupted()){
                synchronized (this){
                    while (dish.breed == null){
                        wait(); // 盤子裏沒有,等生產者生產
                    }
                }
                System.out.println("消費麪包>>>" + dish.breed);
                synchronized (dish.producer){
                    // 消費了盤子中的麪包以後要喚醒生產者生產下一個
                    dish.breed = null;
                    dish.producer.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            System.out.println("Consumer interrupted");
        }
    }
}

public class Dish {

    Breed breed;

    Producer producer = new Producer(this);

    Consumer consumer = new Consumer(this);

    ExecutorService exec = Executors.newCachedThreadPool();

    public Dish() {
        exec.execute(producer);
        exec.execute(consumer);
    }

    public void close(){
        exec.shutdownNow();
    }
    public static void main(String[] args) throws InterruptedException {
        Dish dish = new Dish();
        TimeUnit.MICROSECONDS.sleep(10);
        dish.close();
    }
}

運行結果:

生產了一個麪包Bread: 0
消費麪包>>>Bread: 0
生產了一個麪包Bread: 1
消費麪包>>>Bread: 1
生產了一個麪包Bread: 2
消費麪包>>>Bread: 2
生產了一個麪包Bread: 3
消費麪包>>>Bread: 3
生產了一個麪包Bread: 4
消費麪包>>>Bread: 4
生產了一個麪包Bread: 5
消費麪包>>>Bread: 5
....
生產了一個麪包Bread: 23
Consumer interrupted
相關文章
相關標籤/搜索