第1 章 簡介
》》併發簡史
@@ 作業系統的出現使得計算機每次能執行多個程式,並且不同的程式都在單獨的程序中執行:
作業系統為各個獨立執行的程序分配各種資源,包括記憶體、檔案控制代碼以及安全證書等。
如果需要的話,在不同的程序之間可以通過一些粗粒度的通訊機制來交換資料,包括:
套接字、訊號處理器、共享記憶體、訊號量以及檔案等。
@@ 在計算機中加入作業系統來實現多個程式的同時執行,主要是基於以下原因:
------ 資源利用率
------ 公平性。不同的使用者和程式對於計算機上的資源有著同等的使用權。一種高效的執行
方式是通過粗粒度的時間分片(Time Slicing)使這些使用者和程式能共享計算機資源,而
不是由一個程式從頭執行到尾,然後再啟動下一下程式。
------ 便利性。通常來說,在計算多個任務時,應該編寫多個程式,每個程式執行一個任務
並在必要時互相通訊,這比只編寫一個程式來計算所有任務更容易實現。
@@ 促使程序出現的因素(資源利用率、公平性、便利性等)同樣也促使著執行緒的出現。
@@ 執行緒允許在同一個程序中同時存在多個控制流。
@@ 執行緒會共享程序範圍內的資源,例如記憶體控制代碼和檔案控制代碼,但每個執行緒都有各自的程式計數器
( Program Counter)、棧以及區域性變數等。
@@ 執行緒還提供了一種直觀的分解模式來充分利用多處理器系統中的硬體並行性,而在同一程式
中的多個執行緒也可以被同時排程到多個 CPU 上執行。
@@ 執行緒也被稱為輕量級程序。在大多數現代作業系統中,都是以執行緒為基本的排程單位,而不是
程序。
@@ 如果沒有明確的協同機制,那麼執行緒將彼此獨立執行。
@@ 由於同一個程序中的所有執行緒都將共享程序的記憶體地址空間,因此這些執行緒都能訪問相同
的變數並在同一堆上分配物件,這就需要實現一種比在程序間共享資料粒度更細的資料共享
機制。
@@ 如果沒有明確的同步機制來協同對共享資料的訪問,那麼當一個執行緒正在使用某個變數時,
另一個執行緒可能同時訪問這個變數,這將造成不可預測的結果。
》》執行緒的優勢
@@ 執行緒可以有效地降低程式的開發和維護等成本,同時提升複雜應用程式的效能。
@@ 執行緒能夠將大部分的非同步工作流轉換成序列工作流,因此能更好地模擬人類的工作方式和
互動方式。
@@ 執行緒可以降低程式碼的複雜度,使程式碼更容易編寫、閱讀和維護。
@@ 在 GUI (圖形使用者介面)應用程式中,執行緒可以提高使用者介面的響應靈敏度,而在伺服器
應用程式中,可以提升資源利用率以及系統吞吐量。
@@ 執行緒可以簡化 JVM 的實現,垃圾收集器通常在一個或多個專門的執行緒中執行。
## 發揮多處理器的強大能力
@@ 如果設計正確,多執行緒程式可以通過提高處理器資源的利用率來提升系統吞吐量。
## 建模的簡單性
@@ 如果需要完成多種型別的任務,那麼需要管理不同任務之間的優先順序和執行時間,並在
任務之間進行切換,這將帶來額外的開銷。
@@ 通過使用執行緒,可以將複雜並且非同步的工作流進一步分解為一組簡單並且同步的工作流,
每個工作流在一個單獨的執行緒中執行,並在特定的同步位置進行互動。
補充:
---------- 可以通過一些現有的框架來實現上述的目標,例如 Servlet 和 RMI (遠端方法呼叫)
---------- 框架負責解決一些細節問題,例如請求管理 、 執行緒建立 、 負載平衡,並在正確
的時刻將請求分發給正確的應用程式元件。
---------- 當呼叫 Servlet 的 service 方法來響應 Web 請求時,可以以同步方式來處理這個
請求,就好像它是一個單執行緒程式。這種方式可以簡化元件的開發,並縮短掌握
這種框架的學習時間。
## 非同步事件的簡化處理
@@ 伺服器應用程式在接受來自多個遠端客戶端的套接字連線請求時,如果為每個連線都
分配其各自的執行緒並且使用同步 I / O , 那麼就會降低這類程式的開發難度。
@@ 單執行緒伺服器程式必須使用非阻塞 I / O ,這種 I / O 的複雜性要遠遠高於同步 I / O ,
並且很容易出錯。然而,如果每個請求都擁有自己的處理執行緒,那麼在處理某個請求
時發生的阻塞將不會影響其他請求的處理。
補充:Java類庫需要獲得一組實現非阻塞 I / O 的包 (java.nio)
## 響應更靈敏的使用者介面(GUI)
@@ 傳統的 GUI 應用程式通常都是單執行緒的,從而在程式碼的各個位置都需要呼叫 poll
方法來獲取輸入事件(這種方式將給程式碼帶來極大的混亂),或者通過一個
” 主事件迴圈 “ 來間接的執行應用程式的所有程式碼。
如果在主事件迴圈中呼叫的程式碼需要很長時間才能執行完成,那麼使用者介面
就會” 凍結 “ ,直到程式碼執行完成。這是因為只有當執行控制權返回到主事件迴圈後,
才能執行後續的使用者介面事件。
@@ 在現代的 GUI 框架中,例如 AWT 和 Swing 等工具,都採用一個事件分發執行緒
(EDT)來替代主事件迴圈。
當某個使用者介面事件發生時(例如按下一個按鈕),在事件執行緒中將呼叫應用
程式的事件處理器。
由於大多數 GUI 執行緒都是單執行緒子系統,因此到目前為止仍然存在主事件迴圈,
但它現在處於 GUI 工具的控制下並在其自己的執行緒中執行,而不是在應用程式的
控制下。
@@ 如果事件執行緒中執行的任務都是短暫的,那麼介面的響應靈敏度就較高,因為事件
執行緒能夠很快地處理使用者的動作。
@@ 如果事件執行緒中的任務需要很長的執行時間,例如對一個大型檔案進行拼寫檢查,
或者從網上獲取一個資源,那介面的響應靈敏度就會降低。
更糟糕的是,不僅介面失去響應,而且即使在介面上包含了 ” 取消 “ 按鈕,也
無法取消這個長時間執行的任務,因為事件執行緒只有在執行完該任務後才能響應
” 取消 “按鈕的點選事件。
@@ 如果將長時間執行的任務放在一個單獨的執行緒中執行,那麼事件執行緒就能及時地
處理介面事件,從而使使用者介面具有更高的靈敏度。
》》執行緒帶來的風險
## 安全性問題
@@ 執行緒安全可能是非常複雜的,在沒有充足同步的情況下,多個執行緒中的操作
執行順序是不可預測的,甚至會產生奇怪的結果。
@@ 程式碼示例:
非執行緒安全的數值序列生成器
public class UnsafeSequence {
private int value ;
/** 返回一個唯一的數值 **/
public int getValue( ) {
return value++ ;
}
}
說明:上述類中是一種常見的併發安全問題,稱為競態條件。在多執行緒環境下,
getValue( ) 是否會返回唯一的值,要取決於執行時對執行緒中操作的交替執行方式。
@@ 將上面的程式碼改為執行緒安全的,程式碼如下:(使用同步機制來協同多執行緒的訪問)
public class Sequence {
private int value ;
public synchronized int getValue( ) {
return value++ ;
}
}
## 活躍性問題
@@ 在開發併發程式碼時,一定要注意執行緒安全是不可破壞的。安全性不僅對多執行緒很重要,
對於單執行緒程式同樣重要。
@@ 執行緒還會導致一些在單執行緒程式中不會出現的問題,例如活躍性問題。
@@ 安全性的目標:永遠不發生糟糕的事情。
活躍性的目標:某件正確的事情最終會發生。
(當某個操作無法繼續執行下去時,就會發生活躍性問題。)
## 效能問題
@@ 與活躍性問題密切相關的是效能問題。活躍性意味著某件正確的事情最終會發生,但卻
不夠好,因為我們通常希望正確地事情儘快發生。
@@ 效能問題包括多個方面,例如服務時間過長,響應不靈敏,吞吐率過低,資源消耗過高,
或者可伸縮性較低等。
@@ 在多執行緒程式中,當執行緒排程器臨時掛起活躍執行緒並轉而執行另一個執行緒時,就會頻繁
地出現上下文切換操作(Context Switch),這種操作將帶來極大的開銷:儲存和恢復執行
上下文,丟失區域性性,並且 CPU 時間將更多地花線上程排程而不是執行緒執行上。
@@ 當執行緒共享資料時,必須使用同步機制,而這些機制往往會抑制某些編譯器優化,使記憶體
快取區中的資料無效,以及增加共享記憶體匯流排的同步流量。
》》執行緒無處不在
@@ 開發執行緒安全的類比開發非執行緒安全的類更加謹慎和細緻。
@@ 每個 Java 應用程式都會使用執行緒。
------ 當 JVM 啟動時,它將為 JVM 的內部任務(例如,垃圾收集 、 終結操作等)建立
後臺執行緒,並建立一個主執行緒來執行 main 方法。
------ AWT 和 Swing 的使用者介面框架將建立執行緒來管理使用者介面事件。
------ Timer 將建立執行緒來執行延遲任務。
------ 一些元件框架,例如 Servlet 和 RMI ,都會建立執行緒池並呼叫這些執行緒中的方法。
@@ 幾乎所有的 Java 應用程式都是多執行緒的,因此在使用框架時仍然需要對應用程式狀態
的訪問進行協同。
@@ 框架通過在框架執行緒中呼叫應用程式程式碼將併發性引入到程式中。在程式碼中將不可避免
地訪問應用程式狀態,因此所有訪問這些狀態的程式碼路徑都必須是執行緒安全的。
@@ Timer
-------- Timer 類的作用是使任務在稍後的時刻執行,或者執行一次,或者週期性地執行。
------- 引入 Timer 可能會使序列程式變得複雜,因為 TimerTask 將在 Timer 管理的執行緒
中執行,而不是由應用程式來管理。
------- 如果某個 TimerTask 訪問了應用程式中其他執行緒訪問的資料,那麼不僅 TimerTask
需要以執行緒安全的方式來訪問資料,其他類也必須採用執行緒安全的方式來訪問該資料。
通常,要實現上述目標,最簡單的方式是確保 TimerTask 訪問的物件本身是執行緒
安全的,從而就能把執行緒安全性封裝在共享物件內部。
@@ Servlet 和 JavaServer Page (JSP)
------- Servlet 框架用於部署網頁應用程式以及分發來自 HTTP 客戶端的請求。到達伺服器
的請求可能會通過一個過濾器鏈被分發到正確的 Servlet 或 JSP 。
-------- 每個 Servlet 都表示一個程式邏輯元件,在高吞吐率的網站中,多個客戶端可能同時
請求同一個 Servlet 服務。在 Servlet 規範中, Servlet 同樣需要滿足被多個執行緒同時
呼叫,換句話說, Servlet 需要是執行緒安全地。
-------- Servlet 通常會訪問與其他 Servlet 共享的資訊,例如應用程式中的物件(這些物件
儲存在 ServletContext 中)或者會話中的物件(這些物件儲存在每個客戶端的
HttpSession 中)。
-------- 當一個 Servlet 訪問在多個 Servlet 或者請求中共享的物件時,必須正確地協同對
這些物件的訪問,因為多個請求可能在不同的執行緒中同時訪問這些物件。
--------- Servlet 和 JSP ,以及在 ServletContext 和 HttpSession 等容器中儲存的 Servlet
過濾器和物件等,都必須是執行緒安全的。
@@ 遠端方法呼叫(RMI)
--------- RMI 使程式碼能夠呼叫在其他 JVM 中執行的物件。當通過 RMI 呼叫某個遠端方法時,
傳遞給方法的引數必須被打包(也稱為列集)到一個位元組流中,通過網路傳輸給遠端
JVM ,然後由遠端 JVM 拆包(也稱為散集)並傳遞給方法。
--------- 當 RMI 程式碼呼叫遠端物件時,這個呼叫將在哪個執行緒中執行?
將在一個由 RMI 管理的執行緒中呼叫物件。
---------- 遠端物件必須注意兩個執行緒安全性問題:
%% 正確地協同在多個物件中共享的狀態,以及對遠端物件本身狀態的訪問(由
於同一個物件可能會在多個執行緒中被同時訪問)。
%% 與 Servlet 相同, RMI 物件應該做好被多個執行緒同時呼叫的準備,並且必須
確保自身的執行緒安全性。
@@ Swing 和 AWT
---------- GUI 應用程式的一個固有屬性是非同步性。使用者可以在任意時刻選擇一個選單項
或者按下一個按鈕,應用程式就會及時響應,即使應用程式當時正在執行其他的任務。
---------- Swing 和 AWT 建立了一個單獨的執行緒來處理使用者觸發的事件,並對呈現給使用者
的圖形介面進行更新。
----------- Swing 的一些元件並不是執行緒安全的,例如 JTable 。
但是,Swing 程式通過將所有對 GUI 元件的訪問侷限在事件執行緒中以實現執行緒安全性。
如果某個應用程式希望在事件執行緒之外控制 GUI ,那麼必須將控制 GUI 的程式碼放在
事件執行緒中執行。
----------- 當用戶觸發某個 UI 動作時,在事件執行緒中就會有一個事件處理器被呼叫以執行使用者
請求的操作。
如果事件處理器需要訪問由其他執行緒同時訪問的應用程式狀態(例如編輯某個文件),
那麼這個事件處理器,以及訪問這個狀態的所有其他程式碼,都必須採用一個執行緒安全
的方式來訪問該狀態。