程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java理論與實踐: 描繪線程安全性

Java理論與實踐: 描繪線程安全性

編輯:關於JAVA

在 Joshua Bloch 的那本出色的 Effective Java Programming Language Guide(參閱 參考資料)一書中,第 52 項的標題為“Document Thread Safety” ,在其中他懇請開發人員准確地記錄下類對線程安全性有哪些保證。就像 Bloch 書中的大多數建議一樣,這也是一個反復提到、但很少實現的非常好的建議(就 像 Bloch 在其 Programming Puzzlers 談話中說的“不要像我兄弟那樣寫代碼 ”)。

有多少次您在 Javadoc 中查看一個類,並猜測“這個類是線程安全的嗎?” 。由於缺少明確的記載,讀者可能會對類的線程安全性做出不當的假設。也許他 們將非線程安全的類假定為線程安全的(這真的很糟!),或者假設可以在調用一 個對象的方法之前同步對象以得到線程安全性(這可能是正確的,也可能還不夠 ,最差的情況是,可能只會提供虛幻的線程安全性)。不管在什麼情況下,最好 在文檔中明確寫明,在多個線程中共享類的實例時類的行為是怎樣的。

看一個這種問題的一個例子, java.text.SimpleDateFormat 類不是線程安 全的,但是在 1.4 JDK 之前這並沒有記錄在 Javadoc 中。有多少開發人員錯誤 地創建了 SimpleDateFormat 的靜態實例,並在多個線程中使用它,同時不知道 他們的程序在大負荷下是否能正確運行?不要對您的客戶或者同事做這樣的事情 !

在忘記之前寫下來(要不就離開公司)

一定要在第一次編寫類的時候記錄線程安全性 -- 在編寫它的時候訪問類線 程安全性需求和行為,要比在幾個月後您(或者其他人)再回過頭來看要容易得多 。永遠也不會比在編寫它時更清楚地了解在一個實現中所發生的情況。此外,在 編寫類的時候記錄線程安全性,可以使您對於線程安全性的最初想法得以保留, 因為維護者希望看到這個記錄成為類的說明的一部分。

如果線程安全性是類的一個二元屬性就好了,您只需要記錄類是線程安全還 是線程不安全的。但是很不幸,它不是這麼簡單的。如果類不是線程安全的,是 否可以在每次訪問這個類的對象時通過同步使它成為線程安全的呢?是否有操作 序列不能允許其他線程的介入,因而不僅需要對基本操作同步,而且對於復合操 作也要同步呢?在包含需要自動執行的一組操作的方法之間是否有狀態依賴關系 呢?開發人員要在並發應用程序中使用一個類時需要掌握這些信息。

定義線程安全性

明確定義線程安全性出人意料地困難,大多數定義看上去完全是自我循環。 快速搜索一下 Google,可以找到以下關於線程安全代碼的典型的、但是沒有多 大幫助的定義(或者可以說是描述):

...可以從多個編程線程中調用,無需線程之間不必要的交互。

...可以同時被多個線程調用,不需要調用一方有任何操作。

有這樣的定義,就不奇怪我們對於線程安全性會感到如此迷惑。這些定義比 說“一個類在可以被多個線程安全調用時就是線程安全的”好不了多少,當然, 它的意義就是如此,但是它不能幫助我們區分一個線程安全的類與一個線程不安 全的類。安全的意義是什麼呢?

實際上,所有線程安全的定義都有某種程序的循環,因為它必須符合類的規 格說明 -- 這是對類的功能、其副作用、哪些狀態是有效和無效的、不可變量、 前置條件、後置條件等等的一種非正式的松散描述(由規格說明給出的對象狀態 約束只應用於外部可見的狀態,即那些可以通過調用其公共方法和訪問其公共字 段看到的狀態,而不應用於其私有字段中表示的內部狀態)。

線程安全性

類要成為線程安全的,首先必須在單線程環境中有正確的行為。如果一個類 實現正確(這是說它符合規格說明的另一種方式),那麼沒有一種對這個類的對象 的操作序列(讀或者寫公共字段以及調用公共方法)可以讓對象處於無效狀態,觀 察到對象處於無效狀態、或者違反類的任何不可變量、前置條件或者後置條件的 情況。

此外,一個類要成為線程安全的,在被多個線程訪問時,不管運行時環境執 行這些線程有什麼樣的時序安排或者交錯,它必須仍然有如上所述的正確行為, 並且在調用的代碼中沒有任何額外的同步。其效果就是,在所有線程看來,對於 線程安全對象的操作是以固定的、全局一致的順序發生的。

正確性與線程安全性之間的關系非常類似於在描述 ACID(原子性、一致性、 獨立性和持久性)事務時使用的一致性與獨立性之間的關系:從特定線程的角度 看,由不同線程所執行的對象操作是先後(雖然順序不定)而不是並行執行的。

方法之問的狀態依賴

考慮下面的代碼片段,它迭代一個 Vector 中的元素。盡管 Vector 的所有 方法都是同步的,但是在多線程的環境中不做額外的同步就使用這段代碼仍然是 不安全的,因為如果另一個線程恰好在錯誤的時間裡刪除了一個元素,則 get() 會拋出一個 ArrayIndexOutOfBoundsException 。

Vector v = new Vector();
   // contains race conditions -- may require external synchronization
   for (int i=0; i<v.size(); i++) {
    doSomething(v.get(i));
   }

這裡發生的事情是: get(index) 的規格說明裡有一條前置條件要求 index 必須是非負的並且小於 size() 。但是,在多線程環境中,沒有辦法可以知道上 一次查到的 size() 值是否仍然有效,因而不能確定 i<size() ,除非在上 一次調用了 size() 後獨占地鎖定 Vector 。

更明確地說,這一問題是由 get() 的前置條件是以 size() 的結果來定義的 這一事實所帶來的。只要看到這種必須使用一種方法的結果作為另一種講法的輸 入條件的樣式,它就是一個 狀態依賴,就必須保證至少在調用這兩種方法期間 元素的狀態沒有改變。一般來說,做到這一點的唯一方法在調用第一個方法之前 是獨占性地鎖定對象,一直到調用了後一種方法以後。在上面的迭代 Vector 元 素的例子中,您需要在迭代過程中同步 Vector 對象。

線程安全程度

如上面的例子所示,線程安全性不是一個非真即假的命題。Vector 的方法都 是同步的,並且 Vector 明確地設計為在多線程環境中工作。但是它的線程安全 性是有限制的,即在某些方法之間有狀態依賴(類似地,如果在迭代過程中 Vector 被其他線程修改,那麼由 Vector.iterator() 返回的 iterator 會拋出 ConcurrentModificationException )。

對於 Java 類中常見的線程安全性級別,沒有一種分類系統可被廣泛接受, 不過重要的是在編寫類時盡量記錄下它們的線程安全行為。

Bloch 給出了描述五類線程安全性的分類方法:不可變、線程安全、有條件 線程安全、線程兼容和線程對立。只要明確地記錄下線程安全特性,那麼您是否 使用這種系統都沒關系。這種系統有其局限性 -- 各類之間的界線不是百分之百 地明確,而且有些情況它沒照顧到 -- 但是這套系統是一個很好的起點。這種分 類系統的核心是調用者是否可以或者必須用外部同步包圍操作(或者一系列操作) 。下面幾節分別描述了線程安全性的這五種類別。

不可變

本欄目的普通讀者聽到我贊美不可變性的優點時不會感到意外。不可變的對 象一定是線程安全的,並且永遠也不需要額外的同步。因為一個不可變的對象只 要構建正確,其外部可見狀態永遠也不會改變,永遠也不會看到它處於不一致的 狀態。Java 類庫中大多數基本數值類如 Integer 、 String 和 BigInteger 都 是不可變的。

線程安全

線程安全的對象具有在上面“線程安全”一節中描述的屬性 -- 由類的規格 說明所規定的約束在對象被多個線程訪問時仍然有效,不管運行時環境如何排列 ,線程都不需要任何額外的同步。這種線程安全性保證是很嚴格的 -- 許多類, 如 Hashtable 或者 Vector 都不能滿足這種嚴格的定義。

有條件的線程安全

我們在 7 月份的文件“ 並發集合類”中討論了有條件的線程安全。有條件 的線程安全類對於單獨的操作可以是線程安全的,但是某些操作序列可能需要外 部同步。條件線程安全的最常見的例子是遍歷由 Hashtable 或者 Vector 或者 返回的迭代器 -- 由這些類返回的 fail-fast 迭代器假定在迭代器進行遍歷的 時候底層集合不會有變化。為了保證其他線程不會在遍歷的時候改變集合,進行 迭代的線程應該確保它是獨占性地訪問集合以實現遍歷的完整性。通常,獨占性 的訪問是由對鎖的同步保證的 -- 並且類的文檔應該說明是哪個鎖(通常是對象 的內部監視器(intrinsic monitor))。

如果對一個有條件線程安全類進行記錄,那麼您應該不僅要記錄它是有條件 線程安全的,而且還要記錄必須防止哪些操作序列的並發訪問。用戶可以合理地 假設其他操作序列不需要任何額外的同步。

線程兼容

線程兼容類不是線程安全的,但是可以通過正確使用同步而在並發環境中安 全地使用。這可能意味著用一個 synchronized 塊包圍每一個方法調用,或者創 建一個包裝器對象,其中每一個方法都是同步的(就像 Collections.synchronizedList() 一樣)。也可能意味著用 synchronized 塊包 圍某些操作序列。為了最大程度地利用線程兼容類,如果所有調用都使用同一個 塊,那麼就不應該要求調用者對該塊同步。這樣做會使線程兼容的對象作為變量 實例包含在其他線程安全的對象中,從而可以利用其所有者對象的同步。

許多常見的類是線程兼容的,如集合類 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 類 Connection 和 ResultSet 。

線程對立

線程對立類是那些不管是否調用了外部同步都不能在並發使用時安全地呈現 的類。線程對立很少見,當類修改靜態數據,而靜態數據會影響在其他線程中執 行的其他類的行為,這時通常會出現線程對立。線程對立類的一個例子是調用 System.setOut() 的類。

其他線程安全記錄考慮

線程安全類(以及線程安全性程度更低的的類) 可以允許或者不允許調用者鎖 定對象以進行獨占性訪問。Hashtable 類對所有的同步使用對象的內部監視器, 但是 ConcurrentHashMap 類不是這樣,事實上沒有辦法鎖定一個 ConcurrentHashMap 對象以進行獨占性訪問。除了記錄線程安全程序,還應該記 錄是否某些鎖 -- 如對象的內部鎖 -- 對類的行為有特殊的意義。

通過將類記錄為線程安全的(假設它確實 是線程安全的),您就提供了兩種有 價值的服務:您告知類的維護者不要進行會影響其線程安全性的修改或者擴展, 您還告知類的用戶使用它時可以不使用外部同步。通過將類記錄為線程兼容或者 有條件線程安全的,您就告知了用戶這個類可以通過正確使用同步而安全地在多 線程中使用。通過將類記錄為線程對立的,您就告知用戶即使使用了外部同步, 他們也不能在多線程中安全地使用這個類。不管是哪種情況,您都在潛在的嚴重 問題出現 之前防止了它們,而要查找和修復這些問題是很昂貴的。

結束語

一個類的線程安全行為是其規格說明中的固有部分,應該成為其文檔的一部 分。因為(還)沒有描述類的線程安全行為的聲明式方式,所以必須用文字描述。 雖然 Bloch 的描述類的線程安全程度的五層系統沒有涵蓋所有可能的情況,但 是它是一個很好的起點。如果每一個類都將這種線程行為的程度加入到其 Javadoc 中,那麼可以肯定的是我們大家都會受益。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved