二哥,離你上一篇我去已通過去兩週時間了,這個系列還不打算更新嗎?着急着看呢。java
以上是讀者 Jason 發來的一條信息,不看不知道,一看真的是嚇一跳,上次我去是 4 月 3 號更新的,離如今一個多月了,可不僅是兩週時間啊。可能我本身每天寫,沒以爲時間已通過去這麼久了,是時候帶來新的一篇「我去」了。git
此次沒有代碼 review,是同事小王直接問個人,「青哥,能給我詳細地說一說 synchronized 關鍵字怎麼用嗎?」他問的態度很謙遜,但我仍是忍不住破口大罵:「我擦,小王,你丫的居然不會用 synchronized,我當初是怎麼面試你進來的!」程序員
(我筆名是沉默王二,讀者都叫二哥,但在公司不是的,同事叫我青哥,想知道我真名的,能夠搜《Web全棧開發進階之路》)github
簡單地說,當兩個或者兩個以上的線程同一時間要修改同一個可變的共享數據時,就須要一些保護措施,不然,共享數據修改後的結果大機率會超出你的預期。對於初學者來講,synchronized 關鍵字就是最好用的一種解決方案。web
可能不少初學者不明白,爲何多線程環境下,可變共享變量修改後的結果會超出預期。爲了解釋清楚這一點,來看一個例子。面試
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()
方法寫一個簡單的測試用例。多線程
可能一些初學者還不知道怎麼快速建立測試用例,我這裏就手摸手地現場教學下。併發
第一步,把鼠標移動到類名上,會彈出一個提示框。app
第二步,點擊「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。這是由於多線程環境下,可變的共享數據沒有獲得保護。
這麼說吧,初學者在遇到多線程問題時,只要 synchronized 關鍵字使用得當,問題就可以迎刃而解。記得我剛回洛陽的時候,面試官問我,項目中是怎麼解決併發問題的呢?我就說用 synchronized 關鍵字,至於其餘的一些鎖機制,我那時候還不知道。
嗯,面試官好像也不知道,由於小公司嘛,併發的量級有限,性能也不用考量得太過深刻(大公司的讀者能夠呵呵了)。接下來,就隨我來,一塊兒看看 synchronized 最多見的三種用法吧。
1)直接用在方法上,就像下面這樣:
public synchronized void synchronizedCalculate() {
setSum(getSum() + 1);
}
複製代碼
修改一下測試用例:
@Test
void synchronizedCalculate() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethod summation = new SynchronizedMethod();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::synchronizedCalculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
}
複製代碼
這時候,再運行測試用例就經過了。由於 synchronized 關鍵字會對 SynchronizedMethod 對象進行加鎖,同一時間內只容許一個線程對 sum 進行修改。這就好像有一間屋子,線程進入屋子裏面才能夠對 sum 加 1,而 synchronized 就至關於在門上加了一個鎖,一個線程進去後就鎖上門,修改完 sum 後,下一個線程再進去,其餘線程就在門外候着。
2)用在 static 方法上,就像下面這樣:
public class SynchronizedStaticMethod {
public static int sum;
public synchronized static void synchronizedCalculate() {
sum = sum + 1;
}
}
複製代碼
sum 是一個靜態變量,要修改靜態變量的時候,就須要把方法也變成 static 的。
來新建一個測試用例:
class SynchronizedStaticMethodTest {
@Test
void synchronizedCalculate() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
IntStream.range(0, 1000)
.forEach(count -> service.submit(SynchronizedStaticMethod::synchronizedCalculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, SynchronizedStaticMethod.sum);
}
}
複製代碼
靜態方法上添加 synchronized 的時候就不須要實例化對象了,直接使用類名就能夠引用方法和使用變量了。測試用例也是能夠經過的。
synchronized static 和 synchronized 不一樣的是,前者鎖的是類,同一時間只能有一個線程訪問這個類;後者鎖的是對象,同一時間只能有一個線程訪問方法。
3)用在方法塊上,就像下面這樣:
public void synchronisedThis() {
synchronized (this) {
setSum(getSum() + 1);
}
}
複製代碼
這時候,將 this 傳遞給了 synchronized 代碼塊,當在某個線程中執行這段代碼塊,該線程會獲取 this 對象的鎖,從而使得其餘線程沒法同時訪問該代碼塊。若是方法是靜態的,咱們將傳遞類名代替對象引用,示例以下所示:
public static void synchronisedThis() {
synchronized (SynchronizedStaticMethod.class) {
sum = sum + 1;
}
}
複製代碼
新建一個測試用例:
@Test
void synchronisedThis() throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethod summation = new SynchronizedMethod();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::synchronisedThis));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
}
複製代碼
測試用例和 synchronized 方法的大差不差,運行後也是能夠經過的。二者之間有所不一樣,synchronized 代碼塊的鎖粒度要比 synchronized 方法小一些,由於 synchronized 代碼塊所在的方法裏還能夠有其餘代碼。
好了,我親愛的讀者朋友,以上就是本文的所有內容了,synchronized 的三種用法你必定掌握了吧?以爲文章有點用的話,請微信搜索「沉默王二」第一時間閱讀。
本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。
我是沉默王二,一枚有趣的程序員,關注便可提升學習效率。最後,請無情地點贊、收藏、留言吧,謝謝。