Java 語言提供了靈活的、看上去很簡單的線程功能,使得您很容易在您的應用程式中使用多線程。然而,Java應用程式中的並發編程比看上去要複雜:在 Java 程式中,有一些微妙(也許並不是那麼微妙)方式會造成資料爭用(data race)以及並發問題。在這篇 Java 理論和實踐中,Brian探討了一個常見的線程方面的危險:在構造過程中,允許 this 引用逃脫(escape)。這個看上去沒有什麼危害的做法可以在 Java 程式中造成無法可預料和不期望的結果。
測試和調試多線程程式是極其困難的,因為並發性方面的危險常常不是以一致的方式顯現出來,甚至有時未必會顯現這種危險性。就線程問題的本質而言,大多數這些問題是無法預料的,甚至在某些平台上(如單一處理器系統),或者低於一定的負載,問題可能根本就不出現。由於測試多線程程式的正確性是如此困難,以及尋找錯誤是如此費時,因此從一開始開發應用程式就要在心中牢記線程的安全性,這一點就顯得尤為重要。在本文中,我們將研究一個特殊的安全執行緒方面的問題 ― 在構造過程中,允許 this 引用逃脫(我們稱之為 逃脫的引用問題) ― 該問題引起了一些未曾期望的結果。然後,為了編寫出安全執行緒的建構函式,我們給出一些準則。
遵循“安全構造”技術
剖析器來找出安全執行緒的違例是非常困難的,這需要專門的經驗。幸運的是(也許會感到吃驚),從一開始建立安全執行緒的類並不是那樣的困難,儘管這需要一種其它專門的技巧:規程。大多數並發性錯誤是來自程式員以方便、改善效能或只是一時的懶惰為名企圖違規而造成的。如許多其它並發性問題一樣,在編寫建構函式時,遵循一些簡單的規則就可以避免這個逃脫的引用問題。
危險的爭用狀態
大多數並發性危險歸根結底是由某類 資料爭用引起的。在多個線程或進程正在讀取和寫入一個共用資料項目時,會發生資料爭用或進入 爭用狀態 ,最終結果取決於這些線程的調度次序。清單 1 給出了一個簡單的資料爭用的樣本,其中程式可以列印 0 或者 1,這取決於對線程的調度。
清單 1. 簡單的資料爭用
public class DataRace {
static int a = 0;
public static void main() {
new MyThread().start();
a = 1;
}
public static class MyThread extends Thread {
public void run() {
public void run() {
System.out.println(a);
}
}
}
可以立即調度第二個線程,列印 a 的初始值 0。另一種情形,第二個線程可能 不立即運行,則導致列印值 1。這個程式的輸出取決於您正在使用的 JDK、底層作業系統的發送器或者隨機計時構件。重複運行該程式,會得到不同的結果。
可見度危險
在清單 1 中,除了這個明顯的爭用 ― 第二個線程是在第一個線程將 a 置為 1 之前還是之後開始執行 ― 之外,實際上還有另一種資料爭用。第二種爭用是一種可見度方面的爭用:兩個線程沒有使用同步,而同步能保證線程之間資料更改的可見度。因為沒有同步,如果在第一個線程對 a 賦值完成之後,運行第二個線程,則第二個線程可能或 不可能立即看見第一個線程所做的更改。第二個線程可能看到 a 仍然為 0,即使第一個線程已經將值 1 賦給了 a。這種第二類的資料爭用(在沒有正確同步的情況下,兩個線程正在訪問同一變數)是一種複雜的問題,但幸運的是,每當讀取一個其它線程可能已寫過的變數,或者寫一個接下來可能會被其它線程讀取的變數時,使用同步就可以避免這類資料爭用。在這裡,我們不想進一步探討這類資料爭用,關於這類複雜問題,您可以參閱側欄 “用 Java Memory Model 同步”,也可以參閱 參考資料以擷取更多有關這類複雜問題的詳細資料。