1. 死鎖
死鎖描述了這樣一種情景,兩個或多個線程永久阻塞,互相等待對方釋放資源。下面是一個例子。
Alphone和Gaston是朋友,都很講究禮節。禮節有一個嚴格的規矩,當你向一個朋友鞠躬時,你必須保持鞠躬的姿勢,直到你的朋友有機會回鞠給你。不幸的是,這個規矩沒有算上兩個朋友相互同時鞠躬的可能。
下面的應用例子,DeadLock,模擬了這個可能性。
當DeadLock運行後,兩個線程極有可能阻塞,當它們嘗試調用bowBack方法時。沒有哪個阻塞會結束,因爲每個線程都在等待另一個線程退出bow方法。
2. 飢餓和活鎖
飢餓和活鎖並不如死鎖一般普遍,但它仍然是每個併發程序設計者可能會遇到的問題。
飢餓
飢餓是指當一個線程不能正常的訪問共享資源並且不能正常執行的情況。這通常在共享資源被其他“貪心”的線程長期時發生。舉個例子,假設一個對象提供了一個同步方法,這個方法通常需要執行很長一段時間才返回。如果一個線程經常調用這個方法,那麼其他需要同步的訪問這個對象的線程就經常會被阻塞。
活鎖
一個線程通常會有會響應其他線程的活動。如果其他線程也會響應另一個線程的活動,那麼就有可能發生活鎖。同死鎖一樣,發生活鎖的線程無法繼續執行。然而線程並沒有阻塞——他們在忙於響應對方無法恢復工作。這就相當於兩個在走廊相遇的人:Alphonse向他自己的左邊靠想讓Gaston過去,而Gaston向他的右邊靠想讓Alphonse過去。可見他們阻塞了對方。Alphonse向他的右邊靠,而Gaston向他的左邊靠,他們還是阻塞了對方。
(本部分原文連接,譯文連接,譯者:Greester,校對:鄭旭東)
多線程之間經常需要協同工作,最常見的方式是使用GuardedBlocks,它循環檢查一個條件(通常初始值爲true),直到條件發生變化才跳出循環繼續執行。在使用GuardedBlocks時有以下幾個步驟需要注意:
假設guardedJoy()方法必須要等待另一線程爲共享變量joy設值才能繼續執行。那麼理論上可以用一個簡單的條件循環來實現,但在等待過程中guardedJoy方法不停的檢查循環條件實際上是一種資源浪費。
更加高效的方法是調用Object.wait將當前線程掛起,直到有另一線程發起事件通知(儘管通知的事件不一定是當前線程等待的事件)。
注意:一定要在循環裏面調用wait方法,不要想當然的認爲線程喚醒後循環條件一定發生了改變。
和其他可以暫停線程執行的方法一樣,wait方法會拋出InterruptedException,在上面的例子中,因爲我們關心的是joy的值,所以忽略了InterruptedException。
爲什麼guardedJoy是synchronized方法?假設d是用來調用wait的對象,當一個線程調用d.wait,它必須要擁有d的內部鎖(否則會拋出異常),獲得d的內部鎖的最簡單方法是在一個synchronized方法裏面調用wait。
當一個線程調用wait方法時,它釋放鎖並掛起。然後另一個線程請求並獲得這個鎖並調用Object.notifyAll通知所有等待該鎖的線程。
當第二個線程釋放這個該鎖後,第一個線程再次請求該鎖,從wait方法返回並繼續執行。
注意:還有另外一個通知方法,notify(),它只會喚醒一個線程。但由於它並不允許指定哪一個線程被喚醒,所以一般只在大規模併發應用(即系統有大量相似任務的線程)中使用。因爲對於大規模併發應用,我們其實並不關心哪一個線程被喚醒。
現在我們使用Guardedblocks創建一個生產者/消費者應用。這類應用需要在兩個線程之間共享數據:生產者生產數據,消費者使用數據。兩個線程通過共享對象通信。在這裏,線程協同工作的關鍵是:生產者發佈數據之前,消費者不能夠去讀取數據;消費者沒有讀取舊數據前,生產者不能發佈新數據。
在下面的例子中,數據通過Drop對象共享的一系列文本消息:
Producer是生產者線程,發送一組消息,字符串DONE表示所有消息都已經發送完成。爲了模擬現實情況,生產者線程還會在消息發送時隨機的暫停。
Consumer是消費者線程,讀取消息並打印出來,直到讀取到字符串DONE爲止。消費者線程在消息讀取時也會隨機的暫停。
ProducerConsumerExample是主線程,它啓動生產者線程和消費者線程。
注意:Drop類是用來演示GuardedBlocks如何工作的。爲了避免重新發明輪子,當你嘗試創建自己的共享數據對象時,請查看Java CollectionsFramework中已有的數據結構。如需更多信息,請參考Questions and Exercises。
(本部分原文鏈接,譯文鏈接,譯者:Greenster,校對:鄭旭東)
一個對象如果在創建後不能被修改,那麼就稱爲不可變對象。在併發編程中,一種被普遍認可的原則就是:儘可能的使用不可變對象來創建簡單、可靠的代碼。
在併發編程中,不可變對象特別有用。由於創建後不能被修改,所以不會出現由於線程干擾產生的錯誤或是內存一致性錯誤。
但是程序員們通常並不熱衷於使用不可變對象,因爲他們擔心每次創建新對象的開銷。實際上這種開銷常常被過分高估,而且使用不可變對象所帶來的一些效率提升也抵消了這種開銷。例如:使用不可變對象降低了垃圾回收所產生的額外開銷,也減少了用來確保使用可變對象不出現併發錯誤的一些額外代碼。
接下來看一個可變對象的類,然後轉化爲一個不可變對象的類。通過這個例子說明轉化的原則以及使用不可變對象的好處。
一個同步類的例子
SynchronizedRGB是表示顏色的類,每一個對象代表一種顏色,使用三個整形數表示顏色的三基色,字符串表示顏色名稱。
使用SynchronizedRGB時需要小心,避免其處於不一致的狀態。例如一個線程執行了以下代碼:
如果有另外一個線程在Statement 1之後、Statement2之前調用了color.set方法,那麼myColorInt的值和myColorName的值就會不匹配。爲了避免出現這樣的結果,必須要像下面這樣把這兩條語句綁定到一塊執行:
這種不一致的問題只可能發生在可變對象上。
定義不可變對象的策略
以下的一些規則是創建不可變對象的簡單策略。並非所有不可變類都完全遵守這些規則,不過這不是編寫這些類的程序員們粗心大意造成的,很可能的是他們有充分的理由確保這些對象在創建後不會被修改。但這需要非常複雜細緻的分析,並不適用於初學者。
·不共享可變對象的引用。當一個引用被當做參數傳遞給構造函數,而這個引用指向的是一個外部的可變對象時,一定不要保存這個引用。如果必須要保存,那麼創建可變對象的拷貝,然後保存拷貝對象的引用。同樣如果需要返回內部的可變對象時,不要返回可變對象本身,而是返回其拷貝。
將這一策略應用到SynchronizedRGB有以下幾步:
經過以上這些修改後,我們得到了ImmutableRGB:
死鎖描述了這樣一種情景,兩個或多個線程永久阻塞,互相等待對方釋放資源。下面是一個例子。
Alphone和Gaston是朋友,都很講究禮節。禮節有一個嚴格的規矩,當你向一個朋友鞠躬時,你必須保持鞠躬的姿勢,直到你的朋友有機會回鞠給你。不幸的是,這個規矩沒有算上兩個朋友相互同時鞠躬的可能。
下面的應用例子,DeadLock,模擬了這個可能性。
- static class Friend {
- private final String name;
- public Friend(String name) {
- this.name = name;
- }
- public String getName() {
- return this.name;
- }
- public synchronized void bow(Friend bower) {
- System.out.format("%s: %s"
- + " has bowed to me!%n",
- this.name, bower.getName());
- bower.bowBack(this);
- }
- public synchronized void bowBack(Friend bower) {
- System.out.format("%s: %s"
- + " has bowed back to me!%n",
- this.name, bower.getName());
- }
- }
- public static void main(String[] args) {
- final Friend alphonse =
- new Friend("Alphonse");
- final Friend gaston =
- new Friend("Gaston");
- new Thread(new Runnable() {
- public void run() { alphonse.bow(gaston); }
- }).start();
- new Thread(new Runnable() {
- public void run() { gaston.bow(alphonse); }
- }).start();
- }
- }
當DeadLock運行後,兩個線程極有可能阻塞,當它們嘗試調用bowBack方法時。沒有哪個阻塞會結束,因爲每個線程都在等待另一個線程退出bow方法。
2. 飢餓和活鎖
飢餓和活鎖並不如死鎖一般普遍,但它仍然是每個併發程序設計者可能會遇到的問題。
飢餓
飢餓是指當一個線程不能正常的訪問共享資源並且不能正常執行的情況。這通常在共享資源被其他“貪心”的線程長期時發生。舉個例子,假設一個對象提供了一個同步方法,這個方法通常需要執行很長一段時間才返回。如果一個線程經常調用這個方法,那麼其他需要同步的訪問這個對象的線程就經常會被阻塞。
活鎖
一個線程通常會有會響應其他線程的活動。如果其他線程也會響應另一個線程的活動,那麼就有可能發生活鎖。同死鎖一樣,發生活鎖的線程無法繼續執行。然而線程並沒有阻塞——他們在忙於響應對方無法恢復工作。這就相當於兩個在走廊相遇的人:Alphonse向他自己的左邊靠想讓Gaston過去,而Gaston向他的右邊靠想讓Alphonse過去。可見他們阻塞了對方。Alphonse向他的右邊靠,而Gaston向他的左邊靠,他們還是阻塞了對方。
保護塊(Guarded Blocks)
(本部分原文連接,譯文連接,譯者:Greester,校對:鄭旭東)
多線程之間經常需要協同工作,最常見的方式是使用GuardedBlocks,它循環檢查一個條件(通常初始值爲true),直到條件發生變化才跳出循環繼續執行。在使用GuardedBlocks時有以下幾個步驟需要注意:
假設guardedJoy()方法必須要等待另一線程爲共享變量joy設值才能繼續執行。那麼理論上可以用一個簡單的條件循環來實現,但在等待過程中guardedJoy方法不停的檢查循環條件實際上是一種資源浪費。
- public void guardedJoy() {
- // Simple loop guard. Wastes
- // processor time. Don't do this!
- while(!joy) {}
- System.out.println("Joy has been achieved!");
- }
更加高效的方法是調用Object.wait將當前線程掛起,直到有另一線程發起事件通知(儘管通知的事件不一定是當前線程等待的事件)。
- public synchronized void guardedJoy() {
- // This guard only loops once for each special event, which may not
- // be the event we're waiting for.
- while(!joy) {
- try {
- wait();
- } catch (InterruptedException e) {}
- }
- System.out.println("Joy and efficiency have been achieved!");
- }
注意:一定要在循環裏面調用wait方法,不要想當然的認爲線程喚醒後循環條件一定發生了改變。
和其他可以暫停線程執行的方法一樣,wait方法會拋出InterruptedException,在上面的例子中,因爲我們關心的是joy的值,所以忽略了InterruptedException。
爲什麼guardedJoy是synchronized方法?假設d是用來調用wait的對象,當一個線程調用d.wait,它必須要擁有d的內部鎖(否則會拋出異常),獲得d的內部鎖的最簡單方法是在一個synchronized方法裏面調用wait。
當一個線程調用wait方法時,它釋放鎖並掛起。然後另一個線程請求並獲得這個鎖並調用Object.notifyAll通知所有等待該鎖的線程。
當第二個線程釋放這個該鎖後,第一個線程再次請求該鎖,從wait方法返回並繼續執行。
注意:還有另外一個通知方法,notify(),它只會喚醒一個線程。但由於它並不允許指定哪一個線程被喚醒,所以一般只在大規模併發應用(即系統有大量相似任務的線程)中使用。因爲對於大規模併發應用,我們其實並不關心哪一個線程被喚醒。
現在我們使用Guardedblocks創建一個生產者/消費者應用。這類應用需要在兩個線程之間共享數據:生產者生產數據,消費者使用數據。兩個線程通過共享對象通信。在這裏,線程協同工作的關鍵是:生產者發佈數據之前,消費者不能夠去讀取數據;消費者沒有讀取舊數據前,生產者不能發佈新數據。
在下面的例子中,數據通過Drop對象共享的一系列文本消息:
- public class Drop {
- // Message sent from producer
- // to consumer.
- private String message;
- // True if consumer should wait
- // for producer to send message,
- // false if producer should wait for
- // consumer to retrieve message.
- private boolean empty = true;
- public synchronized String take() {
- // Wait until message is
- // available.
- while (empty) {
- try {
- wait();
- } catch (InterruptedException e) {}
- }
- // Toggle status.
- empty = true;
- // Notify producer that
- // status has changed.
- notifyAll();
- return message;
- }
- public synchronized void put(String message) {
- // Wait until message has
- // been retrieved.
- while (!empty) {
- try {
- wait();
- } catch (InterruptedException e) {}
- }
- // Toggle status.
- empty = false;
- // Store message.
- this.message = message;
- // Notify consumer that status
- // has changed.
- notifyAll();
- }
- }
Producer是生產者線程,發送一組消息,字符串DONE表示所有消息都已經發送完成。爲了模擬現實情況,生產者線程還會在消息發送時隨機的暫停。
- import java.util.Random;
- public class Producer implements Runnable {
- private Drop drop;
- public Producer(Drop drop) {
- this.drop = drop;
- }
- public void run() {
- String importantInfo[] = {
- "Mares eat oats",
- "Does eat oats",
- "Little lambs eat ivy",
- "A kid will eat ivy too"
- };
- Random random = new Random();
- for (int i = 0;
- i < importantInfo.length;
- i++) {
- drop.put(importantInfo[i]);
- try {
- Thread.sleep(random.nextInt(5000));
- } catch (InterruptedException e) {}
- }
- drop.put("DONE");
- }
- }
Consumer是消費者線程,讀取消息並打印出來,直到讀取到字符串DONE爲止。消費者線程在消息讀取時也會隨機的暫停。
- import java.util.Random;
- public class Consumer implements Runnable {
- private Drop drop;
- public Consumer(Drop drop) {
- this.drop = drop;
- }
- public void run() {
- Random random = new Random();
- for (String message = drop.take();
- ! message.equals("DONE");
- message = drop.take()) {
- System.out.format("MESSAGE RECEIVED: %s%n", message);
- try {
- Thread.sleep(random.nextInt(5000));
- } catch (InterruptedException e) {}
- }
- }
- }
ProducerConsumerExample是主線程,它啓動生產者線程和消費者線程。
- public class ProducerConsumerExample {
- public static void main(String[] args) {
- Drop drop = new Drop();
- (new Thread(new Producer(drop))).start();
- (new Thread(new Consumer(drop))).start();
- }
- }
注意:Drop類是用來演示GuardedBlocks如何工作的。爲了避免重新發明輪子,當你嘗試創建自己的共享數據對象時,請查看Java CollectionsFramework中已有的數據結構。如需更多信息,請參考Questions and Exercises。
不可變對象
(本部分原文鏈接,譯文鏈接,譯者:Greenster,校對:鄭旭東)
一個對象如果在創建後不能被修改,那麼就稱爲不可變對象。在併發編程中,一種被普遍認可的原則就是:儘可能的使用不可變對象來創建簡單、可靠的代碼。
在併發編程中,不可變對象特別有用。由於創建後不能被修改,所以不會出現由於線程干擾產生的錯誤或是內存一致性錯誤。
但是程序員們通常並不熱衷於使用不可變對象,因爲他們擔心每次創建新對象的開銷。實際上這種開銷常常被過分高估,而且使用不可變對象所帶來的一些效率提升也抵消了這種開銷。例如:使用不可變對象降低了垃圾回收所產生的額外開銷,也減少了用來確保使用可變對象不出現併發錯誤的一些額外代碼。
接下來看一個可變對象的類,然後轉化爲一個不可變對象的類。通過這個例子說明轉化的原則以及使用不可變對象的好處。
一個同步類的例子
SynchronizedRGB是表示顏色的類,每一個對象代表一種顏色,使用三個整形數表示顏色的三基色,字符串表示顏色名稱。
- public class SynchronizedRGB {
- // Values must be between 0 and 255.
- private int red;
- private int green;
- private int blue;
- private String name;
- private void check(int red,
- int green,
- int blue) {
- if (red < 0 || red > 255
- || green < 0 || green > 255
- || blue < 0 || blue > 255) {
- throw new IllegalArgumentException();
- }
- }
- public SynchronizedRGB(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- public void set(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- synchronized (this) {
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- }
- public synchronized int getRGB() {
- return ((red << 16) | (green << 8) | blue);
- }
- public synchronized String getName() {
- return name;
- }
- public synchronized void invert() {
- red = 255 - red;
- green = 255 - green;
- blue = 255 - blue;
- name = "Inverse of " + name;
- }
- }
使用SynchronizedRGB時需要小心,避免其處於不一致的狀態。例如一個線程執行了以下代碼:
- SynchronizedRGB color =
- new SynchronizedRGB(0, 0, 0, "Pitch Black");
- ...
- int myColorInt = color.getRGB(); //Statement 1
- String myColorName = color.getName(); //Statement 2
如果有另外一個線程在Statement 1之後、Statement2之前調用了color.set方法,那麼myColorInt的值和myColorName的值就會不匹配。爲了避免出現這樣的結果,必須要像下面這樣把這兩條語句綁定到一塊執行:
- synchronized (color) {
- int myColorInt = color.getRGB();
- String myColorName = color.getName();
- }
這種不一致的問題只可能發生在可變對象上。
定義不可變對象的策略
以下的一些規則是創建不可變對象的簡單策略。並非所有不可變類都完全遵守這些規則,不過這不是編寫這些類的程序員們粗心大意造成的,很可能的是他們有充分的理由確保這些對象在創建後不會被修改。但這需要非常複雜細緻的分析,並不適用於初學者。
- 不要提供setter方法。(包括修改字段的方法和修改字段引用對象的方法)
- 將類的所有字段定義爲final、private的。
- 不允許子類重寫方法。簡單的辦法是將類聲明爲final,更好的方法是將構造函數聲明爲私有的,通過工廠方法創建對象。
- 如果類的字段是對可變對象的引用,不允許修改被引用對象。
·不共享可變對象的引用。當一個引用被當做參數傳遞給構造函數,而這個引用指向的是一個外部的可變對象時,一定不要保存這個引用。如果必須要保存,那麼創建可變對象的拷貝,然後保存拷貝對象的引用。同樣如果需要返回內部的可變對象時,不要返回可變對象本身,而是返回其拷貝。
將這一策略應用到SynchronizedRGB有以下幾步:
- SynchronizedRGB類有兩個setter方法。第一個set方法只是簡單的爲字段設值(譯者注:刪掉即可),第二個invert方法修改爲創建一個新對象,而不是在原有對象上修改。
- 所有的字段都已經是私有的,加上final即可。
- 將類聲明爲final的
- 只有一個字段是對象引用,並且被引用的對象也是不可變對象。
經過以上這些修改後,我們得到了ImmutableRGB:
- final public class ImmutableRGB {
- // Values must be between 0 and 255.
- final private int red;
- final private int green;
- final private int blue;
- final private String name;
- private void check(int red,
- int green,
- int blue) {
- if (red < 0 || red > 255
- || green < 0 || green > 255
- || blue < 0 || blue > 255) {
- throw new IllegalArgumentException();
- }
- }
- public ImmutableRGB(int red,
- int green,
- int blue,
- String name) {
- check(red, green, blue);
- this.red = red;
- this.green = green;
- this.blue = blue;
- this.name = name;
- }
- public int getRGB() {
- return ((red << 16) | (green << 8) | blue);
- }
- public String getName() {
- return name;
- }
- public ImmutableRGB invert() {
- return new ImmutableRGB(255 - red,
- 255 - green,
- 255 - blue,
- "Inverse of " + name);
- }
- }
高級併發對象