多個線程執行相同的程序自己不會有安全問題,問題在於訪問了相同的資源。資源能夠是內存區域(變量,數組,對象),系統(數據庫,web 服務)或文件等。實際上多個線程讀取不會變化的資源也不會有問題,問題在於一到多個線程對資源進行寫操做。html
如下給出累加器併發訪問實例,多個線程對同一個累加器進行累加操做。分別打印出各個線程運行中數值先後變化,以及在主線程暫停2s後,給出最終的結果以及預期結果。java
done like this:web
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什麼時候會切換線程運行。此時代碼的運行軌跡相似下面的順序:bash
從主存加載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()
方法。對於臨界區的代碼不加上適當的同步措施將會造成競態條件,其運行結果徹底沒法預估。ide
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial