并發
并發(concurrency)是指CPU在某個時間段內交替處理多任務的能力。每個CPU不可能只顧著執行某個進程,而讓其他進程一直等待被執行。所以,CPU把可執行時間均分成若干份,每個進程執行一份或多份時間后,記錄當前的工作狀態,釋放相關資源并進入等待狀態,讓其他進程搶占CPU等資源。
在并發環境下,由于程序的封閉性被打破,出現了以下特點:
1.并發程序之間有相互制約的關系。直接制約體現在一個程序需要另一個程序的計算結果;間接制約體現在多個進程競爭共享資源。
2.并發程序的執行過程是斷斷續續的。程序需要保留現場,記憶現場指令及執行點。
3.當并發數設置合理并且CPU擁有足夠的處理能力時,并發會提高程序的運行效率。
在Java編程中,并發主要與線程有關。
線程
線程是CPU調度和分派的基本單位,為了更充分地利用CPU資源,一般都會使用多線程進行處理。多線程的作用是提高任務的平均執行速度,但是會導致程序可解性變差,編程難度加大。所以,合適的線程數才能讓CPU資源被充分利用。
每一個線程都有自己的操作棧、程序計數器、局部變量表等資源。同一進程內的所有線程都可以共享該進程的所有資源。
Java提供了兩種形式定義線程類:
1.實現Runnable接口并重寫其中的run()方法。


1 class Consumer implements Runnable { 2 3 private Store store; 4 5 public Consumer(Store store) { 6 this.store = store; 7 } 8 9 @Override 10 public void run() { 11 for (int i = 0; i < 1000; i++) { 12 store.getValue(); 13 } 14 } 15 16 }
2.繼承Thread類并重寫其中的run()方法。


1 class Producer extends Thread { 2 3 private Store store; 4 5 public Producer(Store store) { 6 this.store = store; 7 } 8 9 @Override 10 public void run() { 11 for (int i = 0; i < 1000; i++) { 12 store.setValue((int) (Math.random() * 100)); 13 } 14 } 15 16 }
里氏代換原則對繼承的一個約束是子類不重寫父類的非抽象方法,而Thread類的run()方法不是一個抽象方法,所以繼承Thread類并重寫其中的run()方法就不符合里氏代換原則,該方式不推薦使用。相比之下,實現Runnable接口可以使編程更加靈活,對外暴露的細節也比較少,讓使用者專注于實現線程的run()方法。
線程狀態
線程的生命周期分為以下5種狀態:
新建狀態
新建狀態是線程被創建且未啟動的狀態。也就是說,初始化一個線程對象時,該對象進入新建狀態。
線程對象的初始化分為2種:
1.如果是繼承Thread類的線程類,則該類線程對象可以直接通過new運算進行初始化。
2.如果是實現Runnable接口的線程類,則該類線程對象通過new運算進行初始化后需要包裝為一個Thread對象。
就緒狀態
就緒狀態是線程啟動后運行之前的狀態。即啟動了的線程在準備執行run()方法時的狀態。
線程的啟動是指線程對象調用Thread的start()方法。
運行狀態
運行狀態是線程運行時的狀態,即啟動了的線程在執行run()方法時的狀態。
阻塞狀態
阻塞狀態分以下3種情況:
同步阻塞:缺少資源無法繼續運行。搶占到資源后會退出該狀態。
主動阻塞:主動讓出CPU執行權,即線程執行Thread的sleep()方法之后的狀態。調用sleep()方法時會傳入一個long類型的參數,表示睡眠的時間,單位為毫秒,時間結束時會退出該狀態。
等待阻塞:進入睡眠,即線程執行Object的wait()方法之后的狀態。其他線程執行Object的notify()方法或notifyAll()方法之后會退出該狀態。
終止狀態
終止狀態是線程執行結束或因異常退出后的狀態。
線程同步
線程同步機制的主要任務是,對多個相關線程在執行次序上進行協調,使并發執行的每個線程之間能按照一定的時序共享資源,并能很好地相互合作,從而使程序的執行具有可再現性。
資源的共享分為兩種方式:
互斥共享方式:某些資源例如打印機、磁帶機等,一次只能給一個線程使用,當一個線程申請該資源時,如果該資源有其他線程在使用,則該線程需要等待,直到資源被釋放之后才能申請。
同時訪問方式:某些資源例如磁盤設備等,一次可以給多個線程“同時”訪問,這種“同時”是宏觀上的,實際上還是多個線程交替訪問。
臨界資源指的是一段時間內只能由一個線程訪問的資源,而臨界區指的是每個線程中訪問臨界資源的那部分代碼。顯然,若能保證每個線程互斥地進入自己的臨界區,便可以實現每個線程對臨界資源的互斥訪問。為此,需要在每個線程進入臨界區前需要對訪問的臨界資源進行檢查,如果它是空閑的,則進入臨界區;否則等待,直到臨界資源空閑。具體流程如下:
進入區:檢查臨界資源的狀態,如果空閑,則將其狀態改為被訪問,并進入臨界區;如果被訪問,則循環等待,直到其狀態變為空閑。
臨界區:訪問臨界資源。
退出區:將臨界資源的狀態改為空閑,并釋放臨界資源。
Java提供synchronized關鍵字標識方法或代碼塊,被標識的方法稱為同步方法,被標識的代碼塊稱為同步代碼塊。每個對象都有一個監視器與之關聯。當線程通過該對象執行同步方法或同步代碼塊時,它首先試圖獲取監視器,如果獲取到監視器,則鎖定該對象,防止其他線程通過該對象執行同步方法或同步代碼塊,執行結束后,解鎖該對象并釋放監視器;如果獲取不到監視器,表示有其他線程通過該對象執行同步方法或同步代碼塊,則會進入等待。所以,監視器的作用就相當于進入區和退出區的作用。
例如:定義兩種線程——生產者(Producer)和消費者(Consumer),生產者每次會產生一個數,消費者每次會取出一個數。Producer和Consumer線程對象通過同一個Store對象來調用Store的同步方法。


1 class Store { 2 3 private int value; 4 5 public synchronized int getValue() { 6 System.out.println("-取出" + value); 7 return value; 8 } 9 10 public synchronized void setValue(int value) { 11 this.value = value; 12 System.out.println("放入" + value); 13 } 14 15 }


1 @Test 2 void test() { 3 Store store = new Store(); 4 Thread producer = new Producer(store); // 繼承Thread類的線程類對象的初始化 5 Thread consumer = new Thread(new Consumer(store)); // 實現Runnable接口的線程類對象的初始化 6 producer.start(); 7 consumer.start(); 8 }
部分輸出結果:
當Consumer線程對象調用getValue()方法時,會獲取監視器,鎖定Store對象,直到方法返回后解鎖Store對象,釋放監視器;當Producer線程對象調用setValue()方法時也是如此。所以在創建Producer和Consumer線程對象時需要傳入同一個Store對象。如果傳入不同的Store對象,每一個Store對象都有一個監視器,則起不到鎖定的效果。
根據輸出結果可以發現:取出多次數后才放入一次數,放入多次數后才取出一次數。要實現放入一個數后取出一個數的效果,則需要添加一個標識量。
改進:在Store類中添加一個mutex標識量,當mutex為true時,表示Store內存了一個數,等待Consumer來取;為false時,表示Store內沒有數,等待Producer生產數。


1 class Store { 2 3 private int value; 4 private boolean mutex; // mutex初始值為false,表示沒有數 5 6 public synchronized int getValue() { 7 while (! mutex) { // mutex為false時進入等待 8 try { 9 wait(); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 } 14 System.out.println("-取出" + value); 15 mutex = false; // 取出數后將mutex置為false 16 notify(); 17 return value; 18 } 19 20 public synchronized void setValue(int value) { 21 while (mutex) { // mutex為true時進入等待 22 try { 23 wait(); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 this.value = value; 29 System.out.println("放入" + value); 30 mutex = true; // 放入數后將mutex置為true 31 notify(); 32 } 33 34 }
部分輸出結果: