摘要:Java語言在并發(fā)程序方面的廣泛應用對程序員提出了新的挑戰(zhàn),為了在多線程環(huán)境中開發(fā)出正確的程序,必須考慮線程安全性。本文結(jié)合一個Java程序闡明了這一概念,并介紹了在線程安全性上的一個安全等級,最后總結(jié)出設計線程安全類的幾個有用原則。
關(guān)鍵詞:Java;并發(fā)程序設計;多線程;線程安全性;同步
中圖分類號:TP393文獻標識碼:A文章編號:1009-3044(2008)11-20338-03
1 引言
JAVA并發(fā)控制問題一直是國外學者研究的熱點,特別是中級并發(fā)包java.util.concurrent伴隨J2SE-1.5發(fā)布后,進一步引發(fā)人們對Java并發(fā)程序設計的研究。現(xiàn)在并發(fā)這一概念不僅是專家級的話題,且每個Java開發(fā)者都應該對它有所了解。文章首先對一個非線程安全的實例進行分析,在前人的基礎(chǔ)上創(chuàng)造性地把數(shù)據(jù)庫中的一些并發(fā)理論應用于線程安全性的分析,并解釋了Java并發(fā)中的線程安全性概念,然后介紹了Bloch線程安全性等級,以它為標準對線程安全性進行劃分,最后總結(jié)設計使用線程安全類應注意的問題,對研究Java并發(fā)中的線程安全性有一定的借鑒意義。
2 線程安全性分析
2.1 一個簡單例子
線程安全性(thread safety)沒有一個標準的定義,一般認為,可以被多個線程使用而不危及安全性的類稱為線程安全(thread-safe)的類。該定義并不能幫助我們深刻理解線程安全性,下面來看一個非線程安全性的例子:
class UnsafeUniqueInteger {
private long value=0;
public long getNext() { /* 返回一個唯一的整數(shù)值。*/
value++;
try {//模仿實際工作中花時間
Thread.sleep((long)(Math.random()*100));
} catch (InterruptedException e){}
return value;
}
}
類UnsafeUniqueInteger的getNext方法旨在返回一個從1到n的唯一整數(shù)序列。顯然,在單線程環(huán)境中該類能正確的運行,下面我們在多線程中對其進行測試:
public class Test {
public static void main(String[] args){
final UnsafeUniqueInteger u = new UnsafeUniqueInteger();
new Thread(\"A=\"){//線程A
public void run(){
long i=u.getNext();
while(i <= 20){
try {
Thread.sleep((long)(Math.random()*100));
} catch (InterruptedException e) {}
String s = Thread.currentThread().getName();
System.out.print(s+i+ \"\\");
i=u.getNext(); }// public void run()
}
}.start();
new Thread(\"B=\"){//線程B
public void run(){/*與線程A中的run方法體相同*/}}.start();
}// main()
}//class Test
為了模仿程序的實際運行,在UnsafeUniqueInteger類和Test類中均加入讓程序睡眠0~0.1秒的語句Thread.sleep((long)(Math.random()*100)),以使非線程安全類的問題更容易暴露出來。若不在UnsafeUniqueInteger類中加入該語句,運行結(jié)果可能是正確的,這也說明調(diào)試多線程程序是很困難的,需要一定的技巧和經(jīng)驗。圖1是運行結(jié)果:
從圖中我們可以清楚地看到UnsafeUniqueInteger類產(chǎn)生唯一整數(shù)序列的目的并沒有達到。甚至A線程和B線程都是從2開始打印輸出,可見使用非線程安全的類將構(gòu)造出錯誤的程序。
2.2 深入分析
UnsafeUniqueInteger類的問題是由于某種不幸的運行時調(diào)度:A和B兩個線程可能同時調(diào)用getNext方法并得到同一個值。圖2所示的調(diào)度順序可作為圖1結(jié)果的一種解釋。
在UnsafeUniqueInteger類中,雖然字段value是私有(private)的,但通過getNext可以從外部改變它的值。當多個線程同時調(diào)用getNext方法對value進行修改時,它們之間就會產(chǎn)生數(shù)據(jù)爭用,因此必須對這種共享、易變的字段進行同步。編寫線程安全的代碼,其核心就是對共享、可被修改字段進行操作的方法同步。在上例中,可以用最簡單的synchronized關(guān)鍵字實現(xiàn)同步,需要同步的方法就是修改value字段的方法getNext,則可使該類成為線程安全的。如:public synchronized long getNext() {……}
上面的例子讓我們看到非線程安全的類帶來的錯誤后果,但線程安全性并不是一個安全與不安全的二元屬性,也就是說它并不是簡單的是否安全的問題。
2.3 線程安全性的等級
對于Java類中常見的線程安全性級別,Bloch由強到弱將其劃分為五個等級:
2.3.1 不可變
不可變的對象一定是線程安全的,并且永遠也不需要額外的同步。不可變對象的外部可見字段永遠也不會改變,永遠也不會看到它處于不一致的狀態(tài)。Java類庫中大多數(shù)基本數(shù)值類如Integer、String和BigInteger都是不可變的。
2.3.2 線程安全
類的實例是可變的,但它的所有方法已經(jīng)通過使用足夠的內(nèi)部同步,不管運行時環(huán)境如何調(diào)度,其實例都不需要任何額外的同步。并發(fā)的調(diào)用將會以某種全局一致的方式連續(xù)地執(zhí)行。Random類和Timer類都是線程安全類,然而這種線程安全性保證是很嚴格的,許多類如Hashtable和Vector都不能滿足這種嚴格的定義。
2.3.3 條件線程安全
條件線程安全類對于單獨的操作可以是線程安全的,但是某些操作序列可能需要外部同步。條件線程安全的最常見的例子是遍歷Hashtable和Vector。為了保證其他線程不會在遍歷的時候改變集合,進行迭代的線程應該確保它是獨占性地訪問集合以實現(xiàn)遍歷的完整性。
2.3.4 線程兼容
線程兼容類不是線程安全的,但是可以通過對其對象實例的所有方法調(diào)用進行外部同步,線程兼容類也可在并發(fā)環(huán)境中安全地使用。通常用一個synchronized塊包圍每一個方法調(diào)用,或者創(chuàng)建一個包裝器對象,其中每一個方法都是同步的(如Collections.synchronizedList方法)。許多常見的類是線程兼容的,如集合框架中的ArrayList和HashMap,JDBC中的Connection和ResultSet。
2.3.5 線程對立
線程對立類是那些不管是否調(diào)用了外部同步都不能安全地并發(fā)使用的類。線程對立很少見,當類修改靜態(tài)數(shù)據(jù),而靜態(tài)數(shù)據(jù)會影響在其他線程中執(zhí)行的其他類的行為,這時通常會出現(xiàn)線程對立。比如調(diào)用System.setOut()的類。
3 設計線程安全類原則
線程安全性等級讓我們更深入地認識線程安全性,我們在設計線程安全類時應遵循以下原則:
3.1 將線程安全性寫入文檔
測試和調(diào)試多線程程序是極其困難的,因為并發(fā)性方面的危險常常不是以一致的方式顯現(xiàn)出來,甚至有時未必會顯現(xiàn)這種危險性,因此從一開始開發(fā)應用程序就要在心中牢記線程的安全性,并將類的線程安全性等級記入文檔。
通過將類記錄為線程安全的(假設是線程安全的),就提供了兩種有價值的信息:第一,告知類的維護者不要進行會影響其線程安全性的修改或者擴展;第二,還告知類的用戶使用它時可以不使用外部同步。通過將類記錄為線程兼容或者有條件線程安全的,就告知用戶這個類可以通過正確使用同步而安全地在多線程中使用。通過將類記錄為線程對立的,就告知用戶即使使用了外部同步,他們也不能在多線程中安全地使用這個類。
不管是哪種情況,如在文檔中記下類的線程安全性行為,就可防止?jié)撛诘膰乐貑栴}發(fā)生,而要查找和修復這些問題需要昂貴的代價。
3.2 對多線程中的沖突操作同步
根據(jù)關(guān)系數(shù)據(jù)庫理論,兩個事務(可類比作兩個線程)對同一數(shù)據(jù)的操作可歸結(jié)為:讀-讀、讀-寫、寫-讀、寫-寫四種情況。我們將后三種不能并發(fā)執(zhí)行的操作稱為沖突操作(conflict operation)。為了構(gòu)造“一次編寫,隨處運行”的線程安全類,應遵守以下規(guī)則:不論什么時候,編寫的變量任何中會引起沖突操作,就必須對操作該變量的方法同步。如上例中,兩個線程調(diào)用getNext方法會引起寫-寫沖突,所以就對getNext方法同步。
編寫線程安全代碼的關(guān)鍵是,控制有關(guān)對象狀態(tài)變量(一個對象的狀態(tài)變量包括會引起其外部可見行為變化的任何數(shù)據(jù))的訪問,特別是對共享(shared)、易變(mutable)狀態(tài)的控制[4]。其中共享是指變量可能被多個線程訪問,易變是指在變量的生存期中值可能被改變。Java中最主要的并發(fā)控制機制是synchronized關(guān)鍵字(提供了互斥鎖),也可用volatile變量、顯式鎖(explicit locks)和原子變量(atomic variables)實現(xiàn)同步[4]。
3.3 修正非線程安全類
如果多個線程訪問沒有同步的同一個會引起沖突操作的變量,程序的線程安全性將遭到破壞。修正這一錯誤有三種方法:第一,不要在線程間共享此類變量;第二,將狀態(tài)變量改為不可變的。第三,在每處訪問此類變量的地方使用同步。然而,修正非線程安全類將涉及大量修改,所以一開始就把類設計為線程安全的比把它修改為線程安全的更容易。
4 結(jié)束語
關(guān)于Java中線程安全性的研究已經(jīng)在各個方面展開,如國外學者提出利用元數(shù)據(jù)(meta data)注釋出線程安全性等級的方法[4],它即可以利于類的使用者識別其線程安全等級,又能夠方便靜態(tài)代碼分析工具的分析,不過該方法仍未納入到J2SE1-6類庫中。本文在總結(jié)前人研究成果的基礎(chǔ)上,提出了利用成熟的數(shù)據(jù)庫并發(fā)理論研究Java并發(fā)程序設計,為更好地識別出要同步的方法提供指導。
參考文獻:
[1] Joshua Bloch. Effective Java: Programming Language Guide[M]. Indiana: Addison Wesley, 2001.
[2] Brian Goetz. Java theory and practice: Safe construction techniques. IBM developerWorks forum[EB/OL]. [2003-9-23]. http://www.ibm.com/developerworks/library/j-jtp09263.html.
[3] Andy Wellings. Concurrent and Real-Time Programming in Java[M]. Chichester: John Wiley Sons Ltd., 2004.
[4] Brian Goetz, Tim Peierls,Joshua Bloch, etal. Java Concurrency in Practice[M]. Indiana: Addison Wesley, 2006.
注:本文中所涉及到的圖表、注解、公式等內(nèi)容請以PDF格式閱讀原文