【JAVA併發程式設計實戰】學習小結
第一章 簡介
摘書
執行緒會共享程序範圍內的資源,例如記憶體控制代碼和檔案控制代碼,但每個執行緒都有各自的程式計數器(Program Counter)、棧以及區域性變數等。
在同一個程式中的多個執行緒也可以被同時排程到多個CPU上執行。
第一部分 基礎知識
第二章 執行緒安全性
摘書
Java中的主要同步機制是關鍵字synchronized,它提供了一種獨佔的加鎖方式,但“同步”這個術語還包括volatile型別的變數,顯式鎖(Explicit Lock)以及原子變數。
如果當多個執行緒訪問同一個可變的狀態變數時沒有使用合適的同步,那麼程式就會出現錯誤。有三種方式可以修復這個問題:
- 不線上程之間共享改狀態變數。
- 將狀態變數修改為不可變的變數。
- 在訪問狀態變數時使用同步。
執行緒安全性定義:當多個執行緒訪問某個類時,這個類始終都能表現出正確的行為,那麼就稱這個類是執行緒安全的。
無狀態物件一定是執行緒安全的。
大多數競態條件的本質:基於一種可能失效的觀察結果來做出判斷或者是執行某個計算。這種型別的競態條件成為“先檢查後執行”:首先觀察到某個條件為真(例如檔案X不存在),然後根據這個觀察結果採用相應的動作(建立檔案X),但事實上,在你觀察到這個結果以及開始建立檔案之間,觀察結果可能變得無效(另一個執行緒在這期間建立了檔案X),從而導致了各種問題(未預期的異常、資料被覆蓋、檔案被破壞等)。
假定有兩個操作A和B,如果從執行A的執行緒來看,當另一個執行緒執行B時,要麼將B全部執行完,要麼完全不執行B,那麼A和B對彼此來說是原子的。原子操作是指,對於訪問同一個狀態的所有操作(包括該操作本身)來說,這個操作是一個以原子方式執行的操作。
在實際情況中,應儘可能地使用現有的執行緒安全物件(例如AtomicLong)來管理類的狀態。與非執行緒安全的物件相比,判斷執行緒安全物件的可能狀態及其狀態轉換情況要更為容易,從而也更加容易維護和驗證執行緒安全性。
要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變數。
重入的一種實現方法是,為每個鎖關聯一個獲取計數值和一個所有者執行緒。當計數值為0時,這個鎖就被認為是沒有被任何執行緒持有。當執行緒請求一個未被持有的鎖時,JVM將會記下鎖的持有者,並且將獲取計數值置為1。如果同一個執行緒再次獲取這個鎖,計數值將會遞增,而當執行緒退出同步程式碼塊時,計數器會相應地遞減。當計數值為0時,這個鎖將被釋放。
並非所有資料都需要鎖的保護,只有被多個執行緒同時訪問的可變資料才需要通過鎖來保護。
當執行時間較長的計算或者可能無法快速完成的操作時(例如,網路IO或控制檯IO),一定不要持有鎖。
體會
狀態的理解,我認為是類的成員變數。無狀態物件就是成員變數不能儲存資料,或者是可以儲存資料但是這個資料不可變。無狀態物件是執行緒安全的。如果方法中存在成員變數,就需要對這個成員變數進行相關的執行緒安全的操作。
不要一味地在方法前加synchronized,這可以保證執行緒安全,但是方法的併發功能會減弱,導致本來可以支援併發的方法變成堵塞,導致程式處理速度的變慢。
synchronized包圍的程式碼要儘可能的短,但是要保證有影響的所有成員變數在一起。沒有關係的成員變數可以用多個synchronized包圍。
第三章 物件的共享
摘書
加鎖的含義不僅僅侷限於互斥行為,還包括記憶體可見性。為了確保所有執行緒都能看到共享變數的最新值,所有執行讀操作或者寫操作的執行緒都必須在同一個鎖上同步。
Java語言提供了一種稍弱的同步機制,即volatile變數,用來確保將變數的更新操作通知到其他執行緒。當把變數宣告為volatile型別後,編譯器與執行時都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重新排序。volatile變數不會被快取在暫存器或者對其他處理器不可見的地方,因此在讀取volatile型別的變數時總會返回最新寫入的值。
在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。
volatile變數通常用做某個操作完成、發生中斷或者是狀態的標誌。volatile的語義不足以確保遞增操作(count++)的原子性,除非你能確保只有一個執行緒對變數執行寫操作。
加鎖機制既可以確保可見性又可以確保原子性,而volatile變數只能確保可見性。
當且僅當滿足以下所有條件時,才應該使用volatile變數:
- 對變數的寫入操作不依賴變數的當前值,或者你能確保只有單個執行緒更新變數的值。
- 該變數不會與其他狀態變數一起納入不變形條件中。
- 訪問該變數時不需要加鎖。
“釋出(Publish)”一個物件的意思是指,使物件能夠在當前作用域之外的程式碼中使用。
當某個不應該釋出的物件被髮布時,這種情況就被稱為逸出(Escape)。
不要在構造過程中使this引出逸出。
如果想在建構函式中註冊一個事件監聽器或者啟動執行緒,那麼可以使用一個私有的建構函式和一個公共的工廠方法(Factory Method),從而避免不正確的構造過程。
棧封閉是執行緒封閉的一種特例,在棧封閉中,只有通過區域性變數才能訪問物件。
維持執行緒封閉性的一種更規範方法是使用ThreadLocal,這個類能夠使執行緒中的某個值與儲存值的物件關聯起來。
ThreadLocal物件通常用於防止對可變的單例項物件(Singleton)或全域性變數進行共享。
當滿足以下條件時,物件才是不可變的:
- 物件建立以後其狀態就不能修改。
- 物件的所有域都是final型別。
- 物件是正確建立的(在物件的建立期間,this引用沒有逸出)。
不可變物件一定是執行緒安全的。
要安全地釋出一個物件,物件的引用以及物件的狀態必須同時對其他執行緒可見。一個正確構造的物件可以通過以下方式來安全地釋出:
- 在靜態初始化函式中初始化一個物件引用。
- 將物件的引用儲存到volatile型別的域或者AtomicReferance物件中。
- 將物件的引用儲存到某個正確構造物件的final型別域中。
- 將物件的引用儲存到一個由鎖保護的域中。
在沒有額外的同步的情況下,任何執行緒都可以安全地使用被安全釋出的事實不可變物件。
物件的釋出需求取決於它的可變性:
- 不可變物件可以通過任意機制來發布。
- 事實不可變物件必須通過安全方式來發布。
- 可變物件必須通過安全方式來發布,並且必須是執行緒安全的或者由某個鎖保護起來。
在併發程式中使用和共享物件時,可以使用一些實用的策略,包括:
- 執行緒封閉。執行緒封閉的物件只能由一個執行緒擁有,物件被封閉在該執行緒中,並且只能由這個執行緒修改。
- 只讀共享。在沒有額外同步的情況下,共享的只讀物件可以由多個執行緒併發訪問,但任何執行緒都不能修改它。共享的只讀物件包括不可變物件和事實不可變物件。
- 執行緒安全共享。執行緒安全的物件在其內部實現同步,因此多個執行緒可以通過物件的公有介面來進行訪問而不需要進一步的同步。
- 保護物件。被保護的物件只能通過持有特定的鎖來訪問。保護物件包括封裝在其他執行緒安全物件中的物件,以及已釋出的並且由某個特定鎖保護的物件。
體會
釋出和逸出的理解:就是說一個類中的成員變數或者物件可以被其他的類所引用使用就是釋出,如用static修飾的靜態變數或者是當前呼叫方法的物件。逸出是指該成員變數或物件在本來不應該被多執行緒引用的情況下暴露出去被引用,導致其值可能被錯誤修改的問題。一句話,不要隨便擴大一個類以及內部使用成員變數和方法的作用域。這也是封裝應該考慮的問題。
this逸出:即在構造方法的內部類中啟動另一個執行緒引用了這個物件,但是這時這個物件還沒有構造完成,可能會導致出乎意料的錯誤。解決方法是建立一個工廠方法,然後將構造器設定成私有構造器。
final修改的成員變數需要在構造器在構造器中初始化,否則物件例項化後這個成員變數不能賦值。final修飾的成員變數是引用物件時,這個物件的地址不能修改,但是這個物件的值是可以修改的。
安全釋出一個物件的四種方式的理解,如A類中有B類的引用:
- A的靜態初始化方法,如public static A a = new A(b);這樣的靜態工廠類中,引用B的時候初始化B。
- A類中的B成員變數用volatile b或者是AtomicReferance b這樣修飾。
- A類中的B成員變數用final B b這樣修飾。
- A類中的方法使用到B的時候用synchronized(lock){B…}包圍。
事實不可變物件很簡單的理解就是技術上是可變的,但是在業務邏輯處理中是不會去修改的物件。
第四章 物件的組合
摘書
在設計執行緒安全類的過程中,需要包含以下三個基本要素:
- 找出構成物件狀態的所有變數
- 找出拘束狀態變數的不變形條件
- 建立物件狀態的併發訪問管理策略
將資料封裝在物件內部,可以將資料的訪問限制在物件的方法上,從而更容易確保執行緒在訪問資料時總能持有正確的鎖。
如果一個類是由多個獨立切執行緒安全的狀態變數組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將執行緒安全性委託給底層的狀態變數。
如果一個狀態變數是執行緒安全的,並且沒有任何不變性條件來約束它的值,在變數的操作上也不存在任何不允許的狀態轉換,那麼就可以安全地釋出這個變數。
體會
類中的成員變數如果是獨立的,就是說沒有相互的判斷和依賴關係,並且這個變數的型別是執行緒安全的,如final修飾的ConCurrentHashMap,那麼這些成員變數可以用基本的getter和setter來保障執行緒安全。
在已經是執行緒安全類的物件的方法上加執行緒安全的鎖並不能保證執行緒安全,因為方法上的鎖和物件的鎖不是同一個。要保證安全,可以在方法中物件進行加鎖,或者是重新實現一個類,然後在類中對於操作方法進行執行緒安全的加鎖。
第五章 基礎構建模組
摘書
標準容器的toString()方法將迭代容器,並在每個元素上呼叫toString()來生成容器內容的格式化表示。
容器的hashCode和equals等方法也會間接地執行迭代操作,當容器作為另一個容器的元素或鍵值時,就會出現這種情況。同樣,contailsAll、removeAll和retainAll等方法,以及把容器作為引數的構造器,都會對容器進行迭代。所有這些間接的迭代操作都有可能丟擲ConcurrentModificationException。
Java5.0增加了兩種新的容器型別:Queue和BlockingQueue。Queue用來臨時儲存一組等待處理的元素。它提供了幾種實現,包括:ConcurrentLinkedQueue,這是一個傳統的先進先出佇列,以及PriorityQueue,這是一個(非併發的)優先佇列。Queue上的操作不會阻塞,如果佇列為空,那麼獲取元素的操作間返回空值。雖然可以用List來模擬Queue的行為——事實上,正是通過LinkedList來實現Queue的,當還需要一個Queue的類,因為它能去掉List的隨機訪問需求,從而實現更高效的併發。
BlockingQueue擴充套件了Queue,增加了可阻塞的插入和獲取等操作。如果佇列為空,那麼獲取元素的操作間一直阻塞,知道佇列中出現一個可用的元素。如果佇列已滿(對於有界佇列來說),那麼插入元素的操作將一直阻塞,直到佇列中出現可用的空間。在“生產者——消費者”這種設計模式中,阻塞佇列是非常有用的。
ConcurrentHashMap並不是將每個方法都在同一個鎖上同步並使得每次只能由一個執行緒訪問容器,而是使用一種粒度更細的加鎖機制來實現更大程度的共享,這種機制稱為分段鎖(Lock Striping)。在這種機制中,任意數量的讀取執行緒可以併發地訪問Map,執行讀取操作的執行緒和執行寫入操作的執行緒可以併發地訪問Map,並且一定數量的寫入執行緒可以併發地修改Map。ConcurrentHashMap帶來的結果是,在併發訪問環境下將實現更高的吞吐量,而在單執行緒環境中只損失非常小的效能。
CopyOnWriteArrayList用於替代同步List,在某些情況下它提供了更好的併發效能,並且在迭代期間不需要對容器進行加鎖或複製。(類似地,CopyOnWriteArraySet的作用是替代同步Set)。
僅當迭代操作遠遠多於修改操作時,才應該使用“寫入時複製”容器。
當在程式碼中呼叫了一個將丟擲InterruptedException異常的方法時,你自己的方法也就變成了一個阻塞方法,並且必須要處理對中斷的響應。對於庫程式碼來說,有兩種基本選擇:
- 傳遞InterruptedException。避開這個異常通常是最明智的策略——只需把InterruptedException傳遞給方法的呼叫者。傳遞InterruptedException的方法包括,根本不捕獲該異常,或者捕獲該異常,然後在執行某種簡單的清理工作後再次丟擲這個異常。
- 恢復終端。有時候不能跑出InterruptedException,例如當代碼是Runnable的一部分時。在這些情況下,必須捕獲InterruptedException,並通過呼叫當前執行緒上的interrupt方法恢復中斷狀態,這樣在呼叫棧中更高層的程式碼將看到引發了一箇中斷,如:
public class TaskRunnable implements Runnable {
BlockingQueue<Task> queue;
...
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
//恢復被中斷的狀態
Thread.currentThread().interrupt();
}
}
}
CountDownLatch是一種靈活的閉鎖實現,可以在上述各種情況中使用,它可以使一個或者多個執行緒等待一組事件發生。閉鎖狀態包括一個計數器,該計數器被初始化為一個正數,表示需要等待的事件數量。countDown方法遞減計數器,表示有一個時間已經發生了,而await方法等待計數器到達零,這表示所有需要等待的事件都已經發生。如果計數器的值非零,那麼await會一直阻塞直到計數器為零,或者等待中的執行緒中斷,或者等待超時。
FutureTask也可以用做閉鎖。(FutureTask實現了Future語義,表示一種抽象的可深層結果的計算)。FutureTask表示的計算是通過Callable來實現的,相當於一種可生成結果的Runnable,並且可以處於一下3種狀態:等待執行(Waitting to run),正在執行(Running)和執行完成(Completed)。“執行完成”表示計算的所有可能結束方式,包括正常結束、由於取消而結束和由於異常而結束等。當FutureTask進入完成狀態後,它會用於停止在這個狀態上。
Semaphore中管理著一組虛擬的許可(permit),許可的初始數量可通過建構函式來指定,在執行操作時可以首先獲取許可(只要還有剩餘的許可),並在使用以後釋放許可。如果沒有許可,那麼acquire將阻塞直到有許可(或者直到被中斷或者操作超時)。release方法將返回一個許可給訊號量。計算訊號量的一種簡化形式是二值訊號量,即初始值為1的Semaphore。二值訊號量可以用做互斥體(mutex),並具備不可重入的加鎖語義:誰擁有這個唯一的許可,誰就擁有了互斥鎖。
release返回許可訊號量的實現不包含真正的許可物件,而且Semaphore也不會將許可和執行緒關聯起來,因此在一個執行緒中獲取的許可可以在另一個執行緒中釋放。可以將acquire操作視為是消費一個許可,而release操作是建立一個許可,Semaphore並不受限於它在建立時的初始許可數量。
Semaphore使用示意:
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
this.set = Collection.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdd = false;
try {
wasAdd = set.add(o);
return wasAdd;
}
finally {
if (!wasAdd)
sem.release();
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved();
}
}
- 柵欄(Barrier)類似於閉鎖,它能阻塞一組執行緒直到某個事件發生。柵欄與閉鎖的關鍵區別在於,所有執行緒必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他執行緒。
體會
就算是執行緒安全的容器類,在進行迭代或者是條件運算(如若沒有則新增)時,沒有對容器物件進行加鎖的話,還是會出現執行緒不安全的情況。因此需要在方法內對容器物件進行加鎖。但是這樣的方法有一個問題就是會大幅度降低容器的併發性,吞吐量嚴重降低。
在使用迭代器Iterator對容器進行遍歷的時候,容器內部的計數器發生變化的話,hasNext和next方法會丟擲ConcurrentModificationException。比如在迭代的時候沒有通過迭代器的remove方法。
之前在別的部落格中,或者是看原始碼的時候發現了HashMap、HashTable和ConcurrentHashMap的區別。舉一個廁所的例子,HashMap就是沒有門的廁所,裡面的坑位也是沒有門的,有人要上廁所,就算坑位裡面有人,他也要一起上。HashTable的作用就是在廁所加一個門,所有人在廁所門口排隊,裡面的人安全了,但是外面需要排很長的隊,效能堪憂。ConcurrentHashMap則是在坑位上加了一個門,大家可以進廁所,然後在想要進的坑位前排隊,這樣效能就得到了很大的優化。這也是書中說的分段鎖的概念。具體的實現需要看原始碼和檢視別的部落格書籍獲取。
書中關於CountDownLatch的例子程式碼很有意思,是關於統計多執行緒執行完任務的時間統計。看懂這個例子,CountDownLatch的用法就明瞭了。還有,這個類的名字很有意思,直譯叫做倒計時發射,很形象。
public long timeTasks(int nThreads, final Runnable task) {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException ignored) {}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end - start;
}
第一部分書中小結
- 可變狀態是至關重要的。所有的併發問題都可以歸結為如何協調對併發狀態的訪問。可變狀態越少,就越容易確保執行緒安全性。
- 儘量將域宣告為final,除非需要它們是可變的。
- 不可變物件一定是執行緒安全的。不可變物件能極大地降低併發程式設計的複雜性。它們更為簡單而且安全,可以任意共享而無須使用加鎖或者保護性複製等機制。
- 封裝有助於管理複雜性。在編寫執行緒安全的程式時,雖然可以將所有資料都儲存在全域性變數中,但為什麼要這樣做?將資料封裝在物件中,更易於維持不變形條件:將同步機制封裝在物件中,更易於遵循同步策略。
- 用鎖來保護每個可變變數。
- 當保護同一個不變性條件中的所有變數時,要是用同一個鎖。
- 在執行復合操作期間,要持有鎖。
- 如果從多個執行緒中訪問同一個可變變數時沒有同步機制,那麼程式會出現問題。
- 不要故作聰明地推斷出不需要使用同步。
- 在設計過程中考慮執行緒安全,或者在文件中明確地指出它不是執行緒安全的。
- 將同步策略文件化。
第二部分 結構化併發應用程式
第6章 任務執行
摘書
無限制建立執行緒的不足:
- 執行緒生命週期的開銷非常高。執行緒的建立和銷燬並不是沒有代價的。根據平臺的不同,實際的開銷也有所不同。當執行緒的建立過程都會需要時間,延遲處理的請求,並且需要JVM和作業系統提供一些輔助操作。如果請求的到達率非常高並且請求的處理過程是輕量級的,例如大多數伺服器應用程式就是這種情況,那麼為每個請求建立一個新執行緒將消耗大量的計算資源。
- 資源消耗。活躍的執行緒會消耗系統資源,尤其是記憶體。如果可執行的執行緒數量多於可用處理器數量,那麼有些執行緒將閒置。大量空閒的執行緒會佔用許多記憶體,給垃圾回收器帶來壓力,而且大量執行緒在競爭CPU資源時還將產生其他的效能開銷。如果你已經擁有足夠多的執行緒使所有CPU保持忙碌狀態,那麼再建立更多的執行緒反而會降低效能。
- 穩定性。在可建立執行緒的數量上存在一個限制。這個限制值將隨著平臺的不同而不同,並且受多個因素制約,包括JVM的啟動引數、Thread建構函式中請求的棧大小,以及底層作業系統對執行緒的限制等。如果破壞了這些限制,那麼很可能丟擲OutOfMemoryError異常,要想從這種錯誤中恢復過來是非常危險的,更簡單的辦法是通過構造程式來避免超出這些限制。
每當看到下面這種形式的程式碼時:
new Thread(runnable).start()
並且你希望獲得一種更靈活的執行策略時,請考慮使用Executor來代替Thread。
執行緒池,從字面含義來看,是指管理一組同構工作執行緒的資源池。執行緒池是與工作佇列(Work Queue)密切相關的,其中在工作佇列中儲存了所有等待執行的任務。工作者執行緒(Worker Thread)的任務很簡單:從工作佇列中獲取一個任務,執行任務,然後返回執行緒池並等待下一個任務。
類庫提供了一個靈活的執行緒池以及一些有用的預設配置。可以通過呼叫Executors中的靜態工作方法之一來建立一個執行緒池:
- newFixedThreadPool。newFixedThreadPool將建立一個固定長度的執行緒池,每當提交一個任務時就建立一個執行緒,直到達到執行緒池的最大數量,這時執行緒池的規模將不再變化(如果某個執行緒由於發生了未預期的Exception而結束,那麼執行緒池會補充一個新的執行緒池)。
- newCachedThreadPool。newCachedThreadPool將建立一個可快取的執行緒池,如果執行緒池的當前規模超過了處理需求時,那麼將回收空閒的執行緒,而當需求增加時,則可以新增新的執行緒,執行緒池的規模不存在任何限制。
- newSingleThreadExecutor。newSingleThreadExecutor是一個單執行緒的Executor,它建立單個工作者執行緒來執行任務,如果這個執行緒異常結束,會建立另一個執行緒來替代。newSingleThreadExecutor能確保依照任務在佇列中的順序來序列執行(例如FIFO、LIFO、優先順序)。
單執行緒的Executor還提供了大量的內部同步機制,從而確保了任務執行的任何記憶體寫入操作對於後續任務來說都是可見的。這意味著,即使這個執行緒會不時地被另一個執行緒替代,當物件總是可以安全地封存在“任務執行緒”中。
- newScheduledThreadPool。newScheduledThreadPool建立了一個固定長度的執行緒池,而且以延遲或定時的方式來執行任務,類似於Timer。
體會
- 本章的重點就是建立執行緒,不要通過手動new執行緒和呼叫,而是借用執行緒池的方法。然後介紹了一些執行緒池的初始化和使用方法。
第7章 取消和關閉
摘書
Java沒有提供任何機制來安全地終止執行緒。但它提供了中斷(Interruption),這是一種協作機制,能夠使一個執行緒終止另一個執行緒的當前工作。
每個執行緒都有一個boolean型別的中斷狀態。當中斷執行緒時,這個執行緒的中斷狀態將被設定成true。在Thread中包含了中斷執行緒以及查詢執行緒中斷狀態的方法,如程式所示。interrupt方法能中斷目標執行緒,而isInterrupt方法能返回目標執行緒的中斷狀態。靜態的interrupted方法將清除當前執行緒的中斷狀態,並返回它之前的值,這也是清除中斷狀態的唯一方法。
puiblic class Thread {
public void interrupt() {...}
public boolean isInterrupted() {...}
public static boolean interrupted() {...}
}
阻塞庫方法,例如Thread.sleep和Object.wait等,都會檢查執行緒何時中斷,並且在發現中斷時提前返回,它們在響應中斷時執行的操作包括:清除中斷狀態,丟擲InterruptedException,表示阻塞操作由於中斷而提前結束。JVM並不能保證阻塞方法檢測到中斷的速度,但在實際情況中響應速度還是非常快的。
當執行緒在非阻塞狀態下中斷時,它的中斷狀態將被設定,然後根據將被取消的操作來檢查中斷狀態以判斷髮生了中斷。通過這樣的方法,中斷操作將變得“有粘性”–如果不觸發InterruptedException,那麼中斷狀態將一直保持,直到明確地清除中斷狀態。
呼叫interrupt並不意味著立即停止目標執行緒正在進行的工作,而只是傳遞了請求中斷的訊息。
對中斷操作的正確理解是:它並不會真正地中斷一個正在執行的執行緒,而只是發出中斷請求,然後由執行緒在下一個合適的時刻中斷自己。(這些時刻也被稱為取消點。)有些方法,例如wait/sleep和join等,將嚴格地處理這些請求,當它們收到中斷請求或者在開始執行時發現某個已被設定好的中斷狀態時,將丟擲一個異常。設計良好的方法可以完全忽略這種請求,只要它們能使呼叫程式碼對中斷請求進行某種處理。設計糟糕的方法可能會遮蔽中斷請求,從而導致呼叫棧中的其他程式碼無法對中斷請求做出響應。
通常,中斷是實現取消的最合理方式。
最合理的中斷策略是某種形式的執行緒級(Thread-Level)取消操作或服務級(Service-Level)取消操作:儘快退出,在必要時進行清理,通知某個所有者該執行緒已經退出。
由於每個執行緒擁有各自的中斷策略,因此除非你知道中斷對該執行緒的含義,否則就不應該中斷這個執行緒。
只有實現了執行緒中斷策略的程式碼才可以遮蔽中斷請求。在常規的任務和庫程式碼中都不應該遮蔽中斷請求。
程式清單給出了另一個版本的timedRun:將任務提交給一個ExecutorService,並通過一個定時的Future.get來獲得結果。如果get在返回時丟擲了一個TimeoutException,那麼任務將通過它的Future來取消。如果任務在被取消前就丟擲一個異常,那麼該異常將重新丟擲以便由呼叫者處理異常。在程式清單中還給出了一個良好的程式設計習慣:取消那些不再需要結果的任務。
public static void timeRun(Runnable r, long timeout, TimeUnit unit) throw InterruptedException {
Future<T> task = taskExec.submit(r);
try {
task.get(timeout, unit);
} catch () {
//接下來任務將被取消
} catch () {
//如果在任務中丟擲異常,那麼重新丟擲該異常
throw launderThrowable(e.getCause);
} finally {
//如果任務已經結束,那麼執行取消操作也不會帶來任何影響
task.cancel(true);//如果任務正在執行,那麼將被取消
}
}
並非所有的可阻塞方法或者阻塞機制都能響應中斷;如果一個執行緒由於執行同步的Socket I/O或者等待獲取內建鎖而阻塞,那麼中斷請求只能設定執行緒的中斷狀態,除此之外沒有其他任何作用。對於那些由於執行不可中斷操作而被阻塞的執行緒,可以使用類似於中斷的手段來停止這些 執行緒,但這要求我們必須知道執行緒阻塞的原因。
- Java.io包中的同步Socket I/O。在伺服器應用程式中,最常見的阻塞I/O形式就是對套接字進行讀取和寫入。雖然InputStream和OutputStream中的read和write等方法都不會響應中斷,但通過關閉底層的套接字,可以使得由於執行read或write等方法而被阻塞的執行緒丟擲一個SocketException。
- Java.io包中的同步I/O。當中斷一個正在InterruptibleChannel上等待的執行緒時,將丟擲ClosedByInterruptException並關閉鏈路(這還會使得其他在這條鏈路上阻塞的執行緒同樣丟擲ClosedByInterruptException)。當關閉一個InterruptibleChannel時,將導致所有在鏈路操作上阻塞的執行緒都丟擲AsynchronousCloseException。大多數標準的Channel都實現了InterruptibleChannel。
- Selector的非同步I/O。如果一個執行緒在呼叫Selector.select方法(在java.nio.channels中)時阻塞了,那麼呼叫close或wakeup方法會使執行緒丟擲ClosedSelectorException並提前返回。
- 獲取某個鎖。如果一個執行緒由於等待某個內建鎖而阻塞,那麼將無法響應中斷,因為執行緒認為它肯定會獲得鎖,所以將不會理會中斷請求。但是,在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個鎖的同時仍能響應中斷。
與其他封裝物件一樣,執行緒的所有權是不可傳遞的:應用程式可以擁有服務,服務也可以擁有工作者執行緒,但應用程式並不能擁有工作者執行緒,因此應用程式不能直接停止工作者執行緒。相反,服務應該提供生命週期方法(Lifecycle Method)來關閉它自己以及它所擁有的執行緒。這樣,當應用程式關閉該服務時,服務就可以關閉所有的執行緒了。在ExecutorService中提供了shutdown和shutDownNow等方法。同樣,在其他擁有執行緒的服務中也應該提供類似的關閉機制。
對於持有執行緒的服務,只要服務的存在時間大於建立執行緒的方法的存在時間,那麼就應該提供生命週期方法。
在執行時間較長的應用程式中,通常會為所有執行緒的未捕獲異常指定同一個異常處理器,並且該處理器至少會將異常資訊記錄到日誌中。
此外,守護執行緒通常不能用來替代應用程式管理程式中各個服務的生命週期。
避免使用終結器。
體會
多執行緒的取消策略,可以設定一個volatile型別的變量表示取消狀態,如果有執行緒修改了這個變數為取消,那麼別的變數在執行的時候能夠第一時間獲取到這個資訊,然後停止後續的操作。
但是取消狀態這種玩法有一個問題就是遇到阻塞操作的時候,這個方式就會失效。如在生產者和消費者模式中,生產者通過阻塞佇列BlockingQueue.put方法放置產品到佇列中時,當佇列滿的時候put會阻塞生產者的執行緒,這時將取消標識修改了,此執行緒也不會去檢查取消標識,此執行緒就無法取消這個操作。
前面提到了通過一個取消狀態的標識位來進行判斷,但是這個方法會因為阻塞方法而失效。而中斷機制很好的處理了這個問題。總的來說,就是可以通過執行緒的方法interrupt方法來告知中斷資訊,敏感方法如sleep、wait、join會立即執行,其他的方法則是會在自己決定在取消點,即一個合適的時機進行中斷。
InterruptException異常以前在使用一些執行緒相關的方法時會遇到過,以前的理解就是簡單的如同系統異常的執行緒執行失敗。現在看下來,其實這是一種可控制的異常,只要是反饋當前執行緒被中斷,以及後續相關的操作,比如是結束執行緒的後續操作並返回結果,或者是嘗試重新喚醒執行緒進行後續的操作。
遇到不可中斷的阻塞操作時,就需要對症下藥了。socket在io的時候不能中斷,那麼就先把socket關閉了,再把執行這個方法的執行緒中斷就OK了。還有其他的可以參見書中的舉例。
應用程式、服務和執行緒的關係:應用程式可能會有多個服務,每個服務有相應的執行緒支援,無論是一條還是多條。書中說應用程式不能直接操控執行緒,我的理解是不能別的服務來操控某個服務產生的執行緒。這其實也很好理解,畢竟本服務的執行緒被別的服務操控的話,這個服務就變得不可控了。而服務需要對自己產生的執行緒進行完整的生命週期的維護,從建立、執行到銷燬。
TODO
相關推薦
【JAVA併發程式設計實戰】學習小結
第一章 簡介 摘書 執行緒會共享程序範圍內的資源,例如記憶體控制代碼和檔案控制代碼,但每個執行緒都有各自的程式計數器(Program Counter)、棧以及區域性變數等。 在同一個程式中的多
【java併發程式設計實戰】—–執行緒基本概念
轉自 http://cmsblogs.com/?p=1638 共享和可變 要編寫執行緒安全的程式碼,其核心在於對共享的和可變的狀態進行訪問。 “共享”就意味著變數可以被多個執行緒同時訪問。我們知道系統中的資源是有限的,不同的執行緒對資源都是具有著同等的使用權。有限、公平就意味著競爭
【Java併發程式設計實戰】—–synchronized
在我們的實際應用當中可能經常會遇到這樣一個場景:多個執行緒讀或者、寫相同的資料,訪問相同的檔案等等。對於這種情況如果我們不加以控制,是非常容易導致錯誤的。在java中,為了解決這個問題,引入臨界區概念。所謂臨界區是指一個訪問共用資源的程式片段,而這些共用資源又無法同時被多個執
【Java併發程式設計實戰】—– AQS(四):CLH同步佇列
在【Java併發程式設計實戰】—–“J.U.C”:CLH佇列鎖提過,AQS裡面的CLH佇列是CLH同步鎖的一種變形。其主要從兩方面進行了改造:節點的結構與節點等待機制。在結構上引入了頭結點和尾節點,他們
【Java併發程式設計實戰】-----synchronized
在我們的實際應用當中可能經常會遇到這樣一個場景:多個執行緒讀或者、寫相同的資料,訪問相同的檔案等等。對於這種情況如果我們不加以控制,是非常容易導致錯誤的。在java中,為了解決這個問題,引入臨界區概念。所謂臨界區是指一個訪問共用資源的程式片段,而這些共用資源又無法同時被多個執行緒訪問。 在java中為了實現
Java 併發程式設計實戰 第一部分小結
下列"併發技巧清單" 列舉了第一部分介紹的概念和規則 * 可變狀態是直觀重要的(it's mutable state, stupid) 所有的併發問題都可以歸結為如何協調對併發狀態的訪問,可變狀態越少,就越容易確保執行緒的安全性。 * 儘量將域宣告為fina
《Java併發程式設計實戰》學習筆記之 第3章 物件的共享
1.記憶體可見性 synchronized關鍵字同步有兩方面的作用: (1)實現原子性或者確定臨界區 (2)確保記憶體可見性 所謂記憶體可見性,即當一個執行緒修改了物件狀態後,其他執行緒能夠看到修改後的狀態。 多執行緒程式在沒有同步的情況下,編譯
【 專欄 】- Java併發程式設計實戰
其實我想要 一種美夢睡不著 一種心臟的狂跳 瓦解界線不被撂倒 奔跑 依靠 我心中最想要 看你看過的浪潮 陪你放肆地年少 ——林俊杰【偉大的渺小】 ------------
【JAVA併發程式設計】--學習路線
學習JAVA併發程式設計,有一定的套路。我們需要關注的核心程式碼無非就是jdk下的併發工具包:java.util.concurrent,因此我們也可以在很多地方看到java併發程式設計簡稱為J.U.C程式設計。原始碼可以通過解壓縮jdk安裝目錄下的src.zip包檢視
【JAVA併發程式設計】--為什麼要學習JAVA併發?
我們常常在學習一門新技術之前,都要問自己一遍:為什麼要學習這門技術? 就如當年你是為何投入JAVA的大軍,而非C++\PHP\Phython?拿我自己來講,想法尤其簡單。因為那時JAVA最火啊,用這門技術的企業最多,工作最好找。 哈哈,我相信這
【Java併發程式設計】面試常考的ThreadLocal,超詳細原始碼學習
[toc] > 本文基於JDK1.8 # ThreadLocal是啥?用來幹啥? ```java public class Thread implements Runnable { //執行緒內部區域性變數 ThreadLocal.ThreadLocalMap threadLocals = n
Java併發程式設計實戰 - 學習筆記
第2章 執行緒安全性 1. 基本概念 什麼是執行緒安全性?可以這樣理解:一個類在多執行緒環境下,無論執行時環境怎樣排程,無論多個執行緒之間的執行順序是什麼,且在主調程式碼中不需要進行任何額外的同步,如果該類都能呈現出預期的、正確的行為,那麼該類就是執行緒安全的。 既然這樣,那麼安
【Java併發程式設計】之二十:併發新特性—Lock鎖和條件變數(含程式碼)
簡單使用Lock鎖 Java 5中引入了新的鎖機制——java.util.concurrent.locks中的顯式的互斥鎖:Lock介面,它提供了比synchronized更加廣泛的鎖定操作。Lock介面有3個實現它的類:ReentrantLock、Reetrant
【Java併發程式設計】之一:可重入內建鎖
每個Java物件都可以用做一個實現同步的鎖,這些鎖被稱為內建鎖或監視器鎖。執行緒在進入同步程式碼塊之前會自動獲取鎖,並且在退出同步程式碼塊時會自動釋放鎖。獲得內建鎖的唯一途徑就是進入由這個鎖保護
【Java併發程式設計】之二十二:併發新特性—障礙器CyclicBarrier(含程式碼)
CyclicBarrier(又叫障礙器)同樣是Java 5中加入的新特性,使用時需要匯入java.util.concurrent.CylicBarrier。它適用於這樣一種情況:你希望建立一組任
【Java併發程式設計】之六:Runnable和Thread實現多執行緒的區別(含程式碼)
Java中實現多執行緒有兩種方法:繼承Thread類、實現Runnable介面,在程式開發中只要是多執行緒,肯定永遠以實現Runnable介面為主,因為實現Runnable介面相比繼承Th
【Java併發程式設計】深入分析ConcurrentHashMap(九)
本章是提高教程可能對於剛入門同學來說會有些難度,讀懂本章你需要了解以下知識點:一、Concurrent原始碼分析ConcurrentHashMap是由Segment(桶)、HashEntry(節點)2大資料結構組成。如下圖所示: 1.1 Segment類和屬性//Seg
【java併發程式設計】執行緒池原理分析及ThreadPoolExecutor原始碼實現
執行緒池簡介: 多執行緒技術主要解決處理器單元內多個執行緒執行的問題,它可以顯著減少處理器單元的閒置時間,增加處理器單元的吞吐能力。 假設一個伺服器完成一項任務所需時間為:T1 建立執行緒時間,T2 線上程中執行任務的時間,T3 銷燬執行緒時間。
【Java併發程式設計】之二十三:併發新特性—訊號量Semaphore(含程式碼)
在作業系統中,訊號量是個很重要的概念,它在控制程序間的協作方面有著非常重要的作用,通過對訊號量的不同操作,可以分別實現程序間的互斥與同步。當然它也可以用於多執行緒的控制,我們完全可以通過
【JAVA併發程式設計】--synchronized應用及解析
相信大多數同學在開始接觸併發程式設計的時候,首先了解的就是synchronized關鍵字的修飾,被synchronized修飾的方法或程式碼塊都可以解決多執行緒安全問題。在Java SE1.6版本之前,我們稱之為重量級鎖。因為它在獲取共享鎖的時候是對CPU的獨