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