執行緒安全和資源共享(Thread Safety and Shared Resources)
可以被多個執行緒同時呼叫的安全程式碼叫做執行緒安全。如果一段程式碼是執行緒安全的,它是不會包含競爭條件的。競爭條件只發生在多個執行緒更改共享資源的時候。因此,瞭解 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"); }
在上面例子中的區域性物件的例項沒有被方法 method2 返回,也沒有傳遞給在 someMethod() 方法之外可以訪問這個區域性物件的其他物件。每個執行 someMethod() 方法的執行緒都會建立一個它自己的區域性物件例項然後分配給區域性物件引用。因此,在這兒使用區域性物件 localObject 是執行緒安全的。事實上,整個 someMethod() 方法是執行緒安全的。即使區域性物件的例項 localObject 被當作引數傳遞給同一個類的其他方法內或者其他類來使用它,都是執行緒安全的。唯一例外的是,如果呼叫方法的時候區域性物件 localObject 作為引數被儲存了下來,其他執行緒可以通過某種手段訪問到它,這種情況下該區域性物件不是執行緒安全的。
成員物件
成員物件隨著物件被儲存到共享的堆中。因此,如果兩個執行緒呼叫了同一個物件例項上的同一個方法並且更改了成員物件,這個方法就不是執行緒安全的。下面這個例子中的方法就不是執行緒安全的:
public class NotThreadSafe {
StringBuilder builder = new StringBuilder();
public void 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");
}
}
注意,通過給兩個執行緒的構造方法傳遞了 NotThreadSafe 類的同一個例項的方式共享了物件。因此,兩個執行緒在 NotThreadSafe 類的同一個例項上呼叫了 add() 方法就會導致競爭條件。
然而,如果兩個執行緒同時呼叫了不同例項上的 add() 方法卻不會導致競爭條件。下面的例子和前面的稍有不同:
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
現在,兩個執行緒都有它們自己的 NotThreadSafe 例項,所以它們呼叫 add() 方法時不會互相影響。所以,即使物件不是執行緒安全的通過某種方式來使用它仍然不會導致競爭條件。
執行緒控制逃逸規則
可以使用執行緒控制逃逸規則來判斷程式碼是不是執行緒安全的:
If a resource is created, used and disposed within the control of the same thread, and never escapes the control of this thread, the use of that resource is thread safe.
如果一個資源的建立、使用、釋放都在同一個執行緒的控制下並且這個資源不會逃逸出這個執行緒的控制範圍,那麼對這個資源的使用就是執行緒安全的。
可以是任何共享的資源,比如物件、陣列、檔案、資料庫連線、 socket 等。在 Java 中不需要程式設計師顯式的釋放物件,所以“被釋放(disposed)”的意思是最終沒有地方使用這個物件或者是把這個物件上的引用置為 null 。
即使對一個物件的使用是執行緒安全的,如果這個物件又指向了一個共享的資源,比如一個檔案或資料庫,作為一個整體,應用程式未必就是執行緒安全的。例如,執行緒 1 和執行緒 2 各自都建立了它們自己的資料庫連線 1 和資料庫連線 2 ,每個執行緒使用自己的資料庫連線本身是執行緒安全的。但是,對資料庫連線指向資源的使用未必是執行緒安全的。例如,兩個執行緒都執行了如下的程式碼:
check if record X exists
if not, insert record X
檢查記錄 X 存不存在,如果不存在的話插入記錄 X
如果兩個執行緒同時執行,而且碰巧他們檢查的是同一條記錄,就有兩個執行緒都插入了 X 記錄的風險。下面是可能的情形:
Thread 1 checks if record X exists. Result = no
Thread 2 checks if record X exists. Result = no
Thread 1 inserts record X
Thread 2 inserts record X
這種情況也可能發生在多個執行緒操作共享的檔案或其他資源上。因此,能夠分辨出被一個執行緒控制的物件是資源本身還是被控制的物件僅僅是資源的引用是很重要的。