二哥,離你上一篇我去已經過去兩周時間了,這個系列還不打算更新嗎?着急着看呢。
以上是讀者 Jason 發來的一條資訊,不看不知道,一看真的是吓一跳,上次我去是 4 月 3 号更新的,離現在一個多月了,可不隻是兩周時間啊。可能我自己天天寫,沒覺得時間已經過去這麼久了,是時候帶來新的一篇“我去”了。
這次沒有代碼 review,是同僚小王直接問我的,“青哥,能給我詳細地說一說 synchronized 關鍵字怎麼用嗎?”他問的态度很謙遜,但我還是忍不住破口大罵:“我擦,小王,你丫的竟然不會用 synchronized,我當初是怎麼面試你進來的!”
(我筆名是沉默王二,讀者都叫二哥,但在公司不是的,同僚叫我青哥,想知道我真名的,可以搜《Web全棧開發進階之路》)
簡單地說,當兩個或者兩個以上的線程同一時間要修改同一個可變的共享資料時,就需要一些保護措施,否則,共享資料修改後的結果大機率會超出你的預期。對于初學者來說,synchronized 關鍵字就是最好用的一種解決方案。
01、為什麼需要保護
可能很多初學者不明白,為什麼多線程環境下,可變共享變量修改後的結果會超出預期。為了解釋清楚這一點,來看一個例子。
public class SynchronizedMethod {
private int sum;
public int getSum() {
return sum;
}
public void setSum(int sum) {
this.sum = sum;
}
public void calculate() {
setSum(getSum() + 1);
}
}
SynchronizedMethod 是一個非常簡單的類,有一個私有的成員變量 sum,對應的 getter/setter,以及給 sum 加 1 的 calculate() 方法。
然後,我們來給 calculate() 方法寫一個簡單的測試用例。
可能一些初學者還不知道怎麼快速建立測試用例,我這裡就手摸手地現場教學下。
第一步,把滑鼠移動到類名上,會彈出一個提示框。
第二步,點選「More actions」按鈕,會彈出以下提示框。
第三步,選擇「Create Test」,彈出建立測試用例的對話框。
選擇最新的 JUnit5,如果項目之前沒有引入 JUnit5 依賴的話,IDEA 會提醒你,點選 Fix,IDEA 會自動幫你添加,非常智能化。在對話框中勾選要建立測試用例的方法——calculate()。
點選 OK 按鈕後,IDEA 會在 src 的同級目錄 test 下建立一個名為 SynchronizedMethodTest 的測試類:
class SynchronizedMethodTest {
@Test
void calculate() {
}
}
calculate() 方法上會有一個 @Test 的注解,表示這是一個測試方法。添加具體的代碼,如下所示:
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethod summation = new SynchronizedMethod();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::calculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
1)Executors.newFixedThreadPool() 方法可以建立一個指定大小的線程池服務 ExecutorService。
2)通過 IntStream.range(0, 1000).forEach() 來執行 calculate() 方法 1000 次。
3)通過 assertEquals() 方法進行判斷。
運作該測試用例,結果會是什麼呢?
很不幸,失敗了。預期的值為 1000,但實際的值是 976。這是因為多線程環境下,可變的共享資料沒有得到保護。