高級程式設計師需知的並發編程知識(一)

  • 2020 年 3 月 14 日
  • 筆記

並發編程簡介

並發編程式Java語言的重要特性之一,當然也是最難以掌握的內容。編寫可靠的並發程式是一項不小的挑戰。但是,作為程式設計師的我們,要變得更有價值,就需要啃一些硬骨頭了。因此,理解並發編程的基礎理論和編程實踐,讓自己變得更值錢吧。

使用並發編程的優勢

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

現在,多核CPU已經非常普遍了,普通的家用PC基本都雙核、四核的,何況企業用的伺服器了。如果程式中只有一個執行緒在運行,則最多也只能利用一個CPU資源啊,如果是一個四核的系統,豈不是最多只利用了25%的CPU資源嗎?嚴重的浪費啊!

另外如果存在I/O操作的話,單執行緒的程式在I/O完成之前只能等著了,處理器完成處於空閑狀態,這樣能處理的請求數量就很低了。換成多執行緒就不一樣了,一個執行緒在I/O的時候,另一個執行緒可以繼續運行,處理請求啊,這樣,吞吐量就上來了。

2、方便業務建模

如果在程式中只包含一種類型的任務,那麼比包含多種不同類型的任務的程式要更易於編寫、錯誤更少,也更容易測試。如果在業務建模中,有多種類型的任務場景。我們可以使用多執行緒來解決,讓每個執行緒專門負責一種類型的任務。

通過使用執行緒,可以將負責並且一步的工作流進一步分解為一組簡單並且同步的工作流,每個工作流在一個單獨的執行緒中運行,並在特定的同步位置進行交互。

並發編程帶來的風險

雖然並發編程幫助我們提高了程式的性能,同時也提高對我們程式設計師的要求,因為在編寫並發程式的過程中,一不小心就面臨著多執行緒帶來的風險。這些風險主要是安全性問題、活躍性問題和性能問題。

1、安全性問題

安全性問題可能是非常複雜的,在多執行緒場景中,如果沒有正確地使用同步機制,會導致程式結果的不確定性,這是非常危險的。

比如我們熟知的 count++ 問題

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

上面的程式碼,在單執行緒環境中沒有問題。但是如果是多個執行緒同時訪問getCount方法,則不會得到期望的正確結果。

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

2、活躍性問題

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

多執行緒中的活躍性問題一般指的就是死鎖、飢餓、活鎖等。

3、性能問題

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

使用多執行緒而產生性能問題的根本原因就是,創建執行緒、切換執行緒都是要帶來某種運行時開銷的。如果我們的程式在頻繁的創建執行緒,那很快創建執行緒的消耗將增加,拖累程式整體性能。同時頻繁的執行緒切換,也會產生性能問題。

創建執行緒的幾種方法

在使用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