1. 程式人生 > >Java併發程式設計實戰--筆記二

Java併發程式設計實戰--筆記二

第5章:基礎構建模組

     ConcurrentHashMap 與其他併發容器一起增強了同步容器類:他們提供的迭代器不會丟擲 ConcurrentModificationException ,因此不需要在迭代過程中對容器加鎖。

     ConcurrentHashMap 返回的迭代器具有弱一致性,而非 “及時失敗”。弱一致性的迭代器可以容忍併發的修改,當建立迭代器時會遍歷已有的元素,並可以在迭代器被構造後將修改操作反映給容器。

     儘管有這些改進,但仍然有一些需要權衡的因素。對於一些需要在整個Map 上進行計算的方法,例如 size 和 isEmpty ,這些方法的寓意被略微減弱了以反映容器的併發特性。由於 size 方法返回的結果在計算時可能已經過期了,它實際上只是一個估計值,因此允許 size 返回一個近似值而不是一個精確值。雖然這看上去有些令人不安,但事實上 size 和 isEmpty 這樣的方法在併發環境下的用處很小,因為他們的返回值總是在不斷變化。因此,這些操作的需求被弱化了,以換取對其他更重要操作的效能優化,包括 get、put、containsKey 和 remove等。

     與 Hashtable 和 synchronizedMap 相比,ConcurrentHashMap 有更多的優勢以及更少的劣勢。因此在大多數情況下,用 ConcurrentHashMap 來代替同步 Map 能進一步提高程式碼的可伸縮性。只有當應用程式需要加鎖Map 以進行獨佔訪問時,才能放棄使用 ConcurrentHashMap。

     “寫入時複製(Copy-On-Write)” 容器的執行緒安全性在於,只要正確的釋出一個事實不可變物件,那麼訪問該物件時就不需要進一步的同步。每次修改的時候,都會建立並重新發佈一個新的容器副本,從而實現可變性。“寫入時複製”容器的迭代器保留一個指向底層基礎陣列的引用,這個陣列當前位於迭代器的起始位置,由於他不會被修改,因此在對其進行同步時只需確保陣列內容的可見性。因此,多個執行緒可以對這個容器進行迭代,而不會彼此干擾或者修改容器的執行緒相互干擾。
顯然,每當修改容器時都會複製底層陣列,這需要一定的開銷,特別是當容器的規模較大時。晉檔跌打操作遠遠多於修改操作時,才應該使用“寫入時複製”容器。

     在構建高可靠的應用程式時,有界佇列是一種強大的資源管理工具;它們能夠意志或防止產生過多的工作項,使應用程式在負荷過載的情況下變得更加健壯。

     Thread提供了interrupt方法,用於中斷執行緒或者查詢執行緒是否已經被中斷。每個執行緒都有一個布林型別的屬性,表示執行緒的中斷狀態,當中斷執行緒時將設定這個狀態。

     中斷時一種協作機制。一個執行緒不能強制其他執行緒停止正在執行的操作而去執行其他的操作。當執行緒A中斷B時,A僅僅是要求B在執行到某個可以暫停的地方停止正在執行的操作——前提是如果執行緒B願意停下來。

第6章 任務執行

     任務是一組邏輯工作單元,而執行緒則是使任務非同步執行的機制。在Java類庫中,任務執行的主要抽象不是Thread,而是Executor。

public interface Executor {
    void execute(Runnable command);
}

     雖然Executor是個簡單的介面,但它卻為靈活且強大的非同步任務執行框架提供了基礎,該框架能支援多種不同型別的任務執行策略。它提供了一種標準的方法將任務的提交過程與執行過程解耦開來,並用Runnable來表示任務。Executor的實現還提供了對生命週期的支援,以及統計資訊收集、應用程式管理機制和效能監視等機制。

     Executor基於生產者–消費者模式,提交任務的操作相當於生產者(生成待完成的工作單元),執行任務的執行緒則相當於消費者(執行完這些工作單元)。如果要在程式中實現一個生產者–消費者模式的設計,那麼最簡單的方式通常就是使用Executor。

     類庫提供了一個靈活的執行緒池以及一些有用的預設配置。可以通過呼叫Executors中的靜態工廠方法之一來建立一個執行緒池:

     1、newFixedThreadPool:將建立一個固定長度的執行緒池,每當提交一個任務時就建立一個執行緒,直到達到執行緒池的最大數量,這是執行緒池的規模將不再變化(如果某個執行緒由於發生了未預期的Exception而結束,那麼執行緒池會補充一個新的執行緒)。

     2、newCachedThreadPool:將建立一個可快取執行緒的執行緒池。根據任務按需建立執行緒,並且當任務結束後會將該執行緒快取60秒,如果期間有新的任務到來,則會重用這些執行緒,如果沒有新任務,則這些執行緒會被終止並移除快取。此執行緒池適用於處理量大且短時的非同步任務。

     3、newSingleThreadExecutor:是一個單執行緒的Executor,它建立單個工作者執行緒來執行任務,如果這個執行緒異常結束,會建立另一個執行緒來替代。它還能確保依照任務在佇列中的順序來序列執行。

     4、newScheduledThreadPool:建立了一個固定長度的執行緒池,而且以延遲或定時的方式來執行任務,類似於Timer。

     為了解決執行服務的生命週期問題,Executor擴充套件了ExecutorService介面,添加了一些用於生命週期管理的方法(同時還有一些用於任務提交的便利方法),如下:

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    //......其他用於任務提交的便利方法
}

     ExecutorService的生命週期有3種狀態:執行、關閉和終止。ExecutorService在初始建立時處於執行狀態。shutdown方法將執行平緩關閉的過程:不再接受新的任務,同時等待已經提交的任務執行完成——包括那些還未開始執行的任務。而shutdownNow方法將執行粗暴的關閉過程:它將嘗試取消所有執行中的任務,並且不再啟動佇列中尚未開始執行的任務。

     在ExecutorService關閉後提交的任務將由“拒絕執行處理器(Rejected Execution Handler)”來處理,它會拋棄任務,或者使得execute方法丟擲一個未檢查的RejectedExecutionException。等所有任務都完成後,ExecutorService將進入終止狀態。可以呼叫awaitTermination來等待ExecutorService到達終止狀態,或者通過呼叫isTerminated來輪詢ExecutorService是否已經終止。通常在呼叫awaitTermination之後會立即呼叫shutdown,從而同步關閉ExecutorService。

     Timer的另一個問題是,如果TimerTask丟擲了一個未檢查的異常,那麼Timer將表現出糟糕的行為。Timer執行緒並不捕捉異常,因此當TimerTask丟擲異常時,將終止定時執行緒。這種情況下,Timer也不會恢復執行緒的執行,而是會錯誤的認為整個Timer都被取消了。因此已經被排程單稍微執行的TimerTask將不會再執行,新的任務也不能被排程(這個問題稱為“執行緒洩露[Thread Leake]”)。

     CompletionService將Executor和BlockingQueue的功能融合在一起。你可以將Callable任務提交給它來執行,然後使用類似於佇列操作的take和poll等方法來獲得已完成的結果,而這些結果會在完成時將被封裝為Future。ExecutorCompletionService實現了CompletionService,並將計算部分委託給一個Executor。

     Executor框架將任務提交與執行策略解耦開來,同時還支援多重不同型別的執行策略。當需要建立執行緒來執行任務時,可以考慮使用Executor。要想在將應用程式分解為不同的任務時獲得最大的好處,必須定義清晰的任務邊界。

第7章 取消與關閉

     對中斷操作的正確理解是:它並不會真正的中斷一個正在執行的執行緒,而只是發出中斷請求,然後由執行緒在下一個合適的時刻中斷自己。(這些時刻也被稱為取消點)。有些方法,例如wait、sleep和join等,將嚴格的處理這種請求,當它們收到中斷請求或者在開始執行時發現某個已被設定好的中斷狀態時,將丟擲一個異常。設計良好的方法可以完全忽略這種請求,只要它們能使呼叫程式碼對中斷請求進行某種處理。設計糟糕的方法可能會遮蔽中斷請求,從而導致呼叫棧中國的其它程式碼無法對中斷請求做出響應。

     在使用靜態的interrupted時應該小心,因為它會清除當前執行緒的中斷狀態。如果在呼叫interrupted時返回了true,那麼除非你想遮蔽這個中斷,否則必須對它進行處理——可以丟擲InterruptedException,或者通過再次呼叫interrupt來恢復中斷狀態,如下程式碼所示:

public class TaskRunnable implements Runnable {
    BlockingQueue<Task> queue;
    public void run() {
        try {
            processTask(queue.take());
        } catch (InterruptedException e) {
            // restore interrupted status
            Thread.currentThread().interrupt();
        }
    }
    void processTask(Task task) {
        // Handle the task
    }
    interface Task {
    }
}

     任務不應該對執行該任務的執行緒的中斷策略做出任何假設,除非該任務被專門設計為在服務中執行,並且在這些服務中包含特定的中斷策略。無論任務把中斷視為取消,還是其他某個中斷響應操作,都應該小心地儲存執行執行緒的中斷狀態。如果除了將 InterruptedException 傳遞給呼叫者外還需要執行其他操作,那麼應該在捕獲 InterruptedException 之後恢復中斷狀態:

Thread.currentThread().interrupt();

     由於每個執行緒擁有各自的中斷策略,因此除非你知道中斷對該執行緒的含義,否則就不應該中斷這個執行緒。
只有實現了執行緒中斷策略的程式碼才可以遮蔽中斷請求。在常規的任務和庫程式碼中都不應該遮蔽中斷請求。

     與其他的封裝物件一樣,執行緒的所有權是不可傳遞的:應用程式可以擁有服務,服務也可以擁有工作者執行緒,但應用程式並不能擁有工作者執行緒,因此應用程式不能停止工作者執行緒。相反,服務應該提供生命週期方法,服務就可以關閉所有的執行緒了。這樣,當應用程式關閉服務時,服務就可以關閉所有的執行緒了。在ExecutorService中提供了shutdown和shutdownNow等方法,同樣,在其他擁有執行緒的服務方法中也應該提供類似的關閉機制。

     對於持有執行緒的服務,只要服務的存在時間大於建立執行緒的方法的存在時間,那麼就應該提供生命週期方法。

     冪等(idempotent、idempotence):在程式設計中.一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。

     未檢查異常也叫RuntimeException(執行時異常)。

     未捕獲異常的處理:在執行時間較長的應用程式中,通常會為所有執行緒的未捕獲異常指定同一個異常處理器,並且該處理器至少會將異常資訊記錄到日誌中。

     當一個執行緒由於未捕獲異常而退出時,JVM會把這個事件報告給應用程式提供的UncaughtExceptionHandler異常處理器。如果沒有提供任何異常處理器,那麼預設的行為是將棧追蹤資訊輸出到System.err。

     這些異常處理器中,只有一個將會被呼叫——JVM首先搜尋每個執行緒的異常處理器,若沒有,則搜尋該執行緒的ThreadGroup的異常處理器。ThreadGroup中的預設異常處理器實現是將處理工作逐層委託給上層的ThreadGroup,直到某個ThreadGroup的異常處理器能夠處理該異常,否則一直傳遞到頂層的ThreadGroup。頂層ThreadGroup的異常處理器委託給預設的系統處理器(如果預設的處理器存在,預設情況下為空),否則把棧資訊輸出到System.err。

// UncaughtExceptionHandler介面
public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

     要為執行緒池中的所有執行緒設定一個UncaughtExceptionHandler,需要為ThreadPoolExecutor的建構函式提供一個ThreadFactory。(與所有的執行緒操控一樣,只有執行緒的所有者能夠改變執行緒的UncaughtExceptionHandler。)標準執行緒池允許當發生未捕獲異常時結束執行緒,但由於使用了一個try-finally程式碼塊來接收通知,因此當執行緒結束時,將有新的執行緒來代替它。

     令人困惑的是,只有通過execute提交的任務,才能將它丟擲的異常交給未捕獲異常處理器,而通過submit提交的任務,無論是丟擲的未檢查異常還是已檢查異常,都將被認為是任務返回狀態的一部分。如果一個由submit提交的任務由於丟擲了異常而結束,那麼這個異常將被Funture.get封裝在ExecutionException中重新丟擲。

     守護執行緒

     有時候,你希望建立一個執行緒來執行一些輔助工作,但又不希望這個執行緒阻礙JVM的關閉。在這種情況下就需要使用守護執行緒(Daemon Thread)。

     在JVM啟動時建立的所有執行緒中,除了主執行緒以外,其他的執行緒都是守護執行緒(例如垃圾回收器以及其他執行輔助工作的執行緒)。當建立一個新執行緒時,新執行緒將繼承建立它的執行緒的守護狀態,因此預設情況下,主執行緒建立的所有執行緒都是普通執行緒。

     普通執行緒與守護執行緒之間的差異僅在於當執行緒退出時發生的操作。當一個執行緒退出時,JVM會檢查其他正在執行的執行緒,如果這些執行緒都是守護執行緒,那麼JVM會正常退出操作。當JVM停止時,所有仍然存在的守護執行緒都將被拋棄——既不會執行finally程式碼塊,也不會執行回捲棧,而JVM只是直接退出。

     我們應儘可能少地使用守護執行緒——很少有操作能夠在不進行清理的情況下被安全地拋棄。特別是,如果在守護執行緒中執行可能包含I/O操作的任務,那麼將是一種危險的行為。守護執行緒最好用於執行“內部”任務,例如週期性地從記憶體的快取中移出逾期的資料。

     此外,守護執行緒通常不能用來替代應用程式管理程式中各個服務的生命週期。

個人微信公眾號:
這裡寫圖片描述