允許被多個(gè)線程同時(shí)執(zhí)行的代碼稱作線程安全的代碼。線程安全的代碼不包含競(jìng)態(tài)條件。當(dāng)多個(gè)線程同時(shí)更新共享資源時(shí)會(huì)引發(fā)競(jìng)態(tài)條件。因此,了解 Java 線程執(zhí)行時(shí)共享了什么資源很重要。
局部變量存儲(chǔ)在線程自己的棧中。也就是說,局部變量永遠(yuǎn)也不會(huì)被多個(gè)線程共享。所以,基礎(chǔ)類型的局部變量是線程安全的。下面是基礎(chǔ)類型的局部變量的一個(gè)例子:
public void someMethod(){
long threadSafeInt = 0;
threadSafeInt++;
}
對(duì)象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對(duì)象并沒有存儲(chǔ)在線程的棧內(nèi)。所有的對(duì)象都存在共享堆中。如果在某個(gè)方法中創(chuàng)建的對(duì)象不會(huì)逃逸出(譯者注:即該對(duì)象不會(huì)被其它方法獲得,也不會(huì)被非局部變量引用到)該方法,那么它就是線程安全的。實(shí)際上,哪怕將這個(gè)對(duì)象作為參數(shù)傳給其它方法,只要?jiǎng)e的線程獲取不到這個(gè)對(duì)象,那它仍是線程安全的。下面是一個(gè)線程安全的局部引用樣例:
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
樣例中 LocalObject 對(duì)象沒有被方法返回,也沒有被傳遞給 someMethod()方法外的對(duì)象。每個(gè)執(zhí)行 someMethod()的線程都會(huì)創(chuàng)建自己的 LocalObject 對(duì)象,并賦值給 localObject 引用。因此,這里的 LocalObject 是線程安全的。事實(shí)上,整個(gè) someMethod()都是線程安全的。即使將 LocalObject 作為參數(shù)傳給同一個(gè)類的其它方法或其它類的方法時(shí),它仍然是線程安全的。當(dāng)然,如果 LocalObject 通過某些方法被傳給了別的線程,那它就不再是線程安全的了。
對(duì)象成員存儲(chǔ)在堆上。如果兩個(gè)線程同時(shí)更新同一個(gè)對(duì)象的同一個(gè)成員,那這個(gè)代碼就不是線程安全的。下面是一個(gè)樣例:
public class NotThreadSafe{
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text);
}
}
如果兩個(gè)線程同時(shí)調(diào)用同一個(gè) NotThreadSafe 實(shí)例上的 add()方法,就會(huì)有競(jìng)態(tài)條件問題。例如:
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
public class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run(){
this.instance.add("some text");
}
}
注意兩個(gè) MyRunnable 共享了同一個(gè) NotThreadSafe 對(duì)象。因此,當(dāng)它們調(diào)用 add()方法時(shí)會(huì)造成競(jìng)態(tài)條件。
當(dāng)然,如果這兩個(gè)線程在不同的 NotThreadSafe 實(shí)例上調(diào)用 call()方法,就不會(huì)導(dǎo)致競(jìng)態(tài)條件。下面是稍微修改后的例子:
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
現(xiàn)在兩個(gè)線程都有自己?jiǎn)为?dú)的 NotThreadSafe 對(duì)象,調(diào)用 add()方法時(shí)就會(huì)互不干擾,再也不會(huì)有競(jìng)態(tài)條件問題了。所以非線程安全的對(duì)象仍可以通過某種方式來消除競(jìng)態(tài)條件。
線程控制逃逸規(guī)則可以幫助你判斷代碼中對(duì)某些資源的訪問是否是線程安全的。
如果一個(gè)資源的創(chuàng)建,使用,銷毀都在同一個(gè)線程內(nèi)完成,
且永遠(yuǎn)不會(huì)脫離該線程的控制,則該資源的使用就是線程安全的。
資源可以是對(duì)象,數(shù)組,文件,數(shù)據(jù)庫連接,套接字等等。Java 中你無需主動(dòng)銷毀對(duì)象,所以“銷毀”指不再有引用指向?qū)ο蟆?/p>
即使對(duì)象本身線程安全,但如果該對(duì)象中包含其他資源(文件,數(shù)據(jù)庫連接),整個(gè)應(yīng)用也許就不再是線程安全的了。比如 2 個(gè)線程都創(chuàng)建了各自的數(shù)據(jù)庫連接,每個(gè)連接自身是線程安全的,但它們所連接到的同一個(gè)數(shù)據(jù)庫也許不是線程安全的。比如,2 個(gè)線程執(zhí)行如下代碼:
檢查記錄 X 是否存在,如果不存在,插入 X
如果兩個(gè)線程同時(shí)執(zhí)行,而且碰巧檢查的是同一個(gè)記錄,那么兩個(gè)線程最終可能都插入了記錄:
線程 1 檢查記錄 X 是否存在。檢查結(jié)果:不存在
線程 2 檢查記錄 X 是否存在。檢查結(jié)果:不存在
線程 1 插入記錄 X
線程 2 插入記錄 X
同樣的問題也會(huì)發(fā)生在文件或其他共享資源上。因此,區(qū)分某個(gè)線程控制的對(duì)象是資源本身,還是僅僅到某個(gè)資源的引用很重要。