1 何謂“競態”
之前在學習一篇文章的時候,就看到“競態”,但是不知道什麼意思,文章中也沒有對“競態”做更多的解釋,後來經過一番的探索,終於弄的差不多明白了,今天寫點總結。
首先,我們要明白“競態”是什麼。先說我的結論吧,“競態”就是在多線程的編程中,你在同一段代碼裏輸入了相同的條件,但是會輸出不確定的結果的情況。我不知道這個解釋是不是夠清楚,我們接着往下看,下面我們用一段代碼來解釋一下啊。
出現競態條件的代碼:
public class MineRaceConditionDemo {
private int sharedValue = 0;
private final static int MAX = 1000;
private int raceCondition() {
if (sharedValue < MAX) {
sharedValue++;
} else {
sharedValue = 0;
}
return sharedValue;
}
public static void main(String[] args) {
MineRaceConditionDemo m = new MineRaceConditionDemo();
ExecutorService es = new ThreadPoolExecutor(10,
10,
5,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
es.execute(() -> {
try {
// 這是精髓所在啊,如果沒有這個,那麼要跑好幾次纔會出現競態條件。
// 這個用來模擬程序中別的代碼的處理時間。
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
int num = m.raceCondition();
if (map.get(num) != null) {
System.out.println("the repeat num: " + num);
System.out.println("happen.");
} else {
map.put(num, 0);
}
});
}
es.shutdown();
}
}
以上的代碼是我自己的設計的一段會出現競態條件的代碼,比較簡陋,但是可以說明問題了,你只要運行上面這段代碼,每次的輸出的結果級大概率都是不同的,但是也有以外,比如你的電腦的性能很強,這段代碼也會出現執行正確的情況,也就是啥也不輸出。
比如有的時候輸出這個:
the repeat num: 78
happen.
the repeat num: 229
happen.
the repeat num: 267
happen.
the repeat num: 267
happen.
the repeat num: 498
happen.
有點時候輸出這個:
the repeat num: 25
happen.
the repeat num: 157
happen.
當然,以上的是我的輸出的,你們的輸出肯定也是不同的。
對於上面這些,同一段代碼,對於同樣的輸出,但是程序的輸出有的時候是正確,有的時候是錯誤的,這種情況,我們稱之爲“競態”。最要命的就是,代碼每次輸出不是每次都錯誤,而是你不知道他什麼時候會正確,什麼時候會錯誤。
當然,如果以上的代碼執行的情況就是,啥都不輸出,所有的值都是唯一的。
2 “競態”爲什麼會發生?
“競態”的發生主要是因爲多個線程都對一個共享變量(比如上面的 sharedValue
就屬於共享變量)有讀取-修改的操作。在某個線程讀取共享變量之後,進行相關操作的時候,別的線程把這個變量給改了,從而導致結果出現了錯誤。
什麼樣的代碼模式會發生“競態”
這部分知識主要是來自《Java多線程編程實戰指南 核心篇》。
這裏書中提到,會發生競態條件就是兩個模式:read-modify-write(讀-改-寫)和 check-than-act(檢測而後行動)。
當然,這裏面的都有一個相同的操作過程:某個有讀取這個“共享變量”的操作,然後別的線程有個修改這個變量的操作。這裏有個重點,在多個線程中,起碼有一個線程有更新操作;如果所有的線程都是讀操作,那麼就不存在什麼競態條件。
總體來說,就是要thread1#load - thread2#update。 這種的模式,起碼是是要有兩個線程的,而且其中某個線程肯定是要有更新“共享變量”操作的,另一個線程不管是讀取變量還是更新變量都會出現錯誤(要麼讀取髒數據、要麼丟失更新結果)。
3 如何消除“競態”?
單以上面的操作來說,一般來說有兩種解法方式,
3.1 加鎖
加上synchronized
關鍵字,保證每次只能有一個線程獲取共享變量的使用權。
private synchronized int raceCondition() {
if (sharedValue < MAX) {
sharedValue++;
} else {
sharedValue = 0;
}
return sharedValue;
}
3.2 利用原子操作
利用java的工具包裏的 AtomicInteger
,代替int
,利用原子操作消除“競態”。
private AtomicInteger sharedValue = new AtomicInteger(0);
private final static int MAX = 1000;
private int raceCondition() {
if (sharedValue.get() < MAX) {
return sharedValue.getAndIncrement();
} else {
sharedValue.set(0);
return sharedValue.get();
}
}
以上兩種方法的要義就是保證每個線程在操作“共享變量”的都是原子操作。