多個執行緒執行相同的程式本身不會有安全問題,問題在於訪問了相同的資源。資源可以是記憶體區域(變數,陣列,物件),系統(資料庫,web 服務)或檔案等。實際上多個執行緒讀取不會變化的資源也不會有問題,問題在於一到多個執行緒對資源進行寫操作。
以下給出累加器併發訪問例項,多個執行緒對同一個累加器進行累加操作。分別列印出各個執行緒執行中數值前後變化,以及在主執行緒暫停2s後,給出最終的結果以及預期結果。
執行緒安全問題例項
done like this:
public class ThreadSecurityProblem {
// 累加器
public static class Counter {
private int count = 0;
// 該方法將會產生競態條件(臨界區程式碼)
public void add(int val) {
int result = this.count + val;
System.out.println(Thread.currentThread().getName() + "-" + "before: " + this.count);
this.count = result;
System.out.println(Thread.currentThread().getName() + "-" + "after: " + this.count);
}
public int getCount() {
return this.count;
}
}
// 執行緒排程程式碼
public static class MyRunnable implements Runnable {
private Counter counter;
private int val;
MyRunnable(Counter counter, int val) {
this.counter = counter;
this.val = val;
}
@Override
public void run() {
counter.add(val);
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
// 開啟5個執行緒,呼叫同一個累加器
IntStream.range(1, 6)
.forEach(i -> {
final MyRunnable myRunnable = new MyRunnable(counter, i);
new Thread(myRunnable, "thread-" + i)
.start();
});
Thread.sleep(2000L);
System.out.println(Thread.currentThread().getName() + "-final: " + counter.getCount());
// 預期值
int normalResult = IntStream.range(1, 6)
.sum();
System.out.println(Thread.currentThread().getName() + "-expected: " + normalResult);
}
}
複製程式碼
執行結果:
thread-1-before: 0
thread-3-before: 0
thread-2-before: 0
thread-2-after: 2
thread-3-after: 3
thread-1-after: 1
thread-4-before: 2
thread-4-after: 6
thread-5-before: 6
thread-5-after: 11
main-final: 11
main-expected: 15
如結果所示,執行緒1/2/3先後取得this.count的初始值0,同時進行累加操作(順序無法預估)。執行緒1/2/3中的最後一次累加賦值後this.count變為2,隨後第4個執行緒開始累加賦值this.count變為6,最後第5個執行緒累加賦值this.count變為11.所以5個執行緒執行完畢後的結果為11,並非預期的15.
例項片段
執行緒1/2/3在執行Counter物件的add()方法時,在沒有任何同步機制的情況下
,無法預估作業系統與JVM何時會切換執行緒執行。此時程式碼的執行軌跡類似下面的順序:
從主存載入this.count的值放到各自的工作記憶體中
各自將工作記憶體中的值累加val
將各自工作記憶體中的值寫回主存
複製程式碼
執行緒1/2/3交替情況模擬:
this.count = 0;
執行緒1: 讀取this.count到工作記憶體中,此時this.count為0
執行緒2: 讀取this.count到工作記憶體中,此時this.count為0
執行緒3: 讀取this.count到工作記憶體中,此時this.count為0
執行緒3: cpu將工作記憶體的值更新為3
執行緒2: cpu將工作記憶體的值更新為2
執行緒1: cpu將工作記憶體的值更新為1
執行緒3: 回寫工作記憶體中的值到主存,此時主存中this.count為3
執行緒1: 回寫工作記憶體中的值到主存,此時主存中this.count為1
執行緒2: 回寫工作記憶體中的值到主存,此時主存中this.count為2
最終主存中的this.count被更新為2
複製程式碼
三個執行緒執行完畢後,this.count最後寫回記憶體的值為最後一個執行緒的累加值(例項中為執行緒2,最後回寫到記憶體的值為2)。
總結
多執行緒訪問順序敏感的區域稱為臨界區,該區域程式碼會形成競態條件。如例項ThreadSecurityProblem
中的Counter
物件的add()
方法。對於臨界區的程式碼不加上適當的同步措施將會形成競態條件,其執行結果完全無法預估。