二哥,離你上一篇我去已經過去兩周時間了,這個系列還不打算更新嗎?着急着看呢。
以上是讀者 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。這是因為多線程環境下,可變的共享數據沒有得到保護。
02、synchronized 的用法
這么說吧,初學者在遇到多線程問題時,只要 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。
我是沉默王二,一枚有趣的程序員,關注即可提高學習效率。最后,請無情地點贊、收藏、留言吧,謝謝。