
面試官拆解四個同步:從入門到精通的避坑指南
看了一堆教程還是不會寫項目?別急,問題往往出在基礎(chǔ)概念沒吃透。很多開發(fā)者覺得“四個同步”只是背個名詞,直到面試被問懵,或者上線后數(shù)據(jù)錯亂才后悔。這篇文章帶你從入門到精通,徹底搞懂這四個核心機制,讓你下次面試穩(wěn)如老狗。
考點梳理:面試官到底在問什么
在Java并發(fā)編程中,“四個同步”通常指synchronized、ReentrantLock、CountDownLatch、CyclicBarrier。雖然名字里都有“同步”,但它們的底層邏輯、適用場景和API設(shè)計哲學(xué)截然不同。
面試官問這個,不是在考你背誦定義,而是在考察你對并發(fā)控制粒度和線程協(xié)作模式的理解。很多候選人只答得出“它們都能讓線程等待”,卻分不清“獨占鎖”和“柵欄同步”的本質(zhì)區(qū)別。一旦深入追問“為什么這里用Latch不用Barrier”,或者“synchronized的鎖升級過程”,立馬露餡。
你需要明確,這四個工具分別解決了并發(fā)編程中的三大核心問題:互斥訪問(防止臟讀臟寫)、線程等待(主線程等待子線程)、線程協(xié)作(多個線程同時等待某個時間點)。搞清楚這一點,你就已經(jīng)超過了60%的競爭者。
標(biāo)準(zhǔn)答法:結(jié)構(gòu)化表達(dá)是關(guān)鍵
面試回答要有條理,建議采用“定義+底層原理+適用場景+優(yōu)缺點”的四段式結(jié)構(gòu)。
1. synchronized
這是Java關(guān)鍵字級別的同步機制。早期JDK 1.5之前,它是唯一的選擇?,F(xiàn)在它的底層實現(xiàn)已經(jīng)優(yōu)化為偏向鎖、輕量級鎖、重量級鎖的漸進(jìn)式升級。核心點:JVM內(nèi)置支持,無需手動釋放,適合短臨界區(qū)。
痛點:不可中斷,無法公平調(diào)度,性能在高競爭下下降明顯。2. ReentrantLock
這是java.util.concurrent包下的顯式鎖。它提供了比synchronized更豐富的功能,比如可中斷的獲取鎖、公平鎖、條件變量(Condition)。核心點:API靈活,支持超時,適合復(fù)雜業(yè)務(wù)邏輯。
痛點:必須手動unlock,如果忘記釋放會導(dǎo)致死鎖,性能略低于優(yōu)化后的synchronized。3. CountDownLatch
這是一個一次性屏障。它允許一個或多個線程等待其他線程完成一組操作。核心點:count只能減不能增,用完即廢。
場景:主線程等待N個子任務(wù)全部完成,比如并行加載多個數(shù)據(jù)源。4. CyclicBarrier
這是一個可復(fù)用的屏障。它讓一組線程全部到達(dá)某個屏障點,然后才能繼續(xù)執(zhí)行。核心點:可重置,支持在屏障點執(zhí)行額外任務(wù)。
場景:多線程分階段計算,比如多輪迭代訓(xùn)練,每輪結(jié)束后需要匯總結(jié)果。記憶技巧:
synchronized是“傻瓜式”互斥,ReentrantLock是“全能型”互斥,CountDownLatch是“單向等待”,CyclicBarrier是“雙向會合”。
代碼實現(xiàn):別只看不練,手敲才有感覺
光說不練假把式。下面這段代碼模擬了一個典型的并發(fā)場景:4個線程并行計算數(shù)據(jù),計算完成后,主線程匯總結(jié)果。 這里我們對比使用CountDownLatch和CyclicBarrier的區(qū)別。
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;public class SyncDemo {private static final int THREAD_COUNT = 4;private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);public static void main(String[] args) throws InterruptedException, BrokenBarrierException {System.out.println(=== 測試 CountDownLatch: 主線程等待子線程 ===);testCountDownLatch();System.out.println(\n=== 測試 CyclicBarrier: 子線程之間等待 ===);testCyclicBarrier();executor.shutdown();}// 場景1:主線程等待所有子線程完成private static void testCountDownLatch() throws InterruptedException {CountDownLatch latch = new CountDownLatch(THREAD_COUNT);long startTime = System.currentTimeMillis();for (int i = 1; i = THREAD_COUNT; i++) {final int taskId = i;executor.submit(() - {try {System.out.println(Thread.currentThread().getName() + 開始處理任務(wù) + taskId);Thread.sleep((long)(Math.random() * 1000)); // 模擬耗時操作System.out.println(Thread.currentThread().getName() + 完成任務(wù) + taskId);} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {latch.countDown(); // 關(guān)鍵:減少計數(shù)}});}// 主線程阻塞,直到計數(shù)為0latch.await();long endTime = System.currentTimeMillis();System.out.println(所有任務(wù)完成,總耗時: + (endTime - startTime) + ms);}// 場景2:所有子線程互相等待,都完成后一起執(zhí)行下一階段private static void testCyclicBarrier() throws BrokenBarrierException, InterruptedException {AtomicInteger phase = new AtomicInteger(0);CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () - {// 當(dāng)所有線程到達(dá)屏障時,執(zhí)行這個Runnablephase.incrementAndGet();System.out.println( 第 + phase.get() + 輪所有線程就緒,開始下一階段 );});for (int i = 1; i = THREAD_COUNT; i++) {final int taskId = i;executor.submit(() - {try {// 階段1System.out.println(Thread.currentThread().getName() + 階段1: 處理數(shù)據(jù) + taskId);Thread.sleep((long)(Math.random() * 500));// 等待其他線程都完成階段1barrier.await();// 階段2 (只有所有線程都通過屏障后才會執(zhí)行)System.out.println(Thread.currentThread().getName() + 階段2: 匯總結(jié)果 + taskId);Thread.sleep((long)(Math.random() * 500));// 如果還有階段3,可以繼續(xù) barrier.await()} catch (InterruptedException | BrokenBarrierException e) {Thread.currentThread().interrupt();}});}// 等待線程池任務(wù)完成Thread.sleep(2000);}
}逐行解析關(guān)鍵點:CountDownLatch的finally塊:注意代碼中l(wèi)atch.countDown()放在finally里。這是生產(chǎn)環(huán)境的最佳實踐。如果任務(wù)執(zhí)行中拋出異常,如果不放在finally,計數(shù)永遠(yuǎn)不會減到0,主線程將永久阻塞,導(dǎo)致線程池耗盡。
CyclicBarrier的Runnable參數(shù):構(gòu)造函數(shù)中的第二個參數(shù)是一個回調(diào)函數(shù)。它只在所有線程調(diào)用await()后執(zhí)行一次。這在需要“同步點”執(zhí)行額外邏輯(如打印日志、重置狀態(tài))時非常有用。
異常處理:CyclicBarrier.await()會拋出BrokenBarrierException。如果某個線程在等待時被中斷或超時,屏障會“破損”,其他等待線程會立即拋出異常。這是與CountDownLatch的重要區(qū)別,后者一個線程異常不影響其他線程繼續(xù)等待。追問與延伸:高階玩家的戰(zhàn)場
面試官滿意你的基礎(chǔ)回答后,通常會追問以下三個高頻問題。
追問1:synchronized和ReentrantLock在性能上有區(qū)別嗎?
答:在低競爭環(huán)境下,JDK 6之后的synchronized通過鎖升級優(yōu)化,性能與ReentrantLock相當(dāng)。但在高競爭環(huán)境下,ReentrantLock由于可以設(shè)置公平鎖和可中斷獲取,避免了線程無限期等待,性能表現(xiàn)更可控。此外,ReentrantLock的lock()和unlock()是用戶空間代碼,而synchronized依賴JVM內(nèi)置指令,后者在簡單場景下開銷更小。
追問2:為什么CountDownLatch不能重置,而CyclicBarrier可以?
答:這是設(shè)計意圖決定的。CountDownLatch通常用于一次性的啟動或關(guān)閉信號,比如服務(wù)器啟動時等待所有模塊初始化完成。這種場景下,重置沒有意義,反而容易引發(fā)狀態(tài)不一致。而CyclicBarrier常用于循環(huán)迭代算法,如蒙特卡洛模擬、多輪機器學(xué)習(xí)訓(xùn)練,每輪都需要所有線程同步,因此必須支持復(fù)用。
追問3:如果線程在await()時拋出了業(yè)務(wù)異常,該怎么辦?
答:對于CountDownLatch,必須在finally中調(diào)用countDown(),確保計數(shù)歸零。對于CyclicBarrier,如果某個線程拋出異常,屏障會被破壞,其他線程也會收到BrokenBarrierException。此時需要捕獲異常并決定是重新創(chuàng)建Barrier還是終止任務(wù)。在實際項目中,建議使用CompletableFuture或ForkJoinPool來替代這種復(fù)雜的同步邏輯,它們提供了更好的異常傳播機制。
避坑指南:不要混用鎖:不要在一個線程用synchronized,另一個線程用ReentrantLock保護同一資源,這會導(dǎo)致死鎖或數(shù)據(jù)不一致。
注意超時:CountDownLatch和CyclicBarrier的await()都有帶超時的版本。生產(chǎn)環(huán)境務(wù)必使用超時版本,防止因某個線程卡死導(dǎo)致整個應(yīng)用阻塞。
官方文檔參考:建議查閱Java SE 17官方文檔中的java.util.concurrent章節(jié),里面詳細(xì)描述了各個類的線程安全性保證和邊界條件,這是最權(quán)威的參考。記憶口訣:四句真言記心間
為了在面試緊張時能快速回憶,送你一個記憶口訣:
“辛同死鎖難釋放,重入鎖活可中斷?!?“倒計一次不回頭,循環(huán)屏障能復(fù)用?!毙镣╯ynchronized):死鎖風(fēng)險,不可中斷,難釋放(指手動釋放困難,因為它是自動釋放,但一旦死鎖就難搞)。
重入鎖(ReentrantLock):靈活,可中斷,可公平。
倒計(CountDownLatch):一次性的,不回頭(不能重置)。
循環(huán)屏障(CyclicBarrier):能復(fù)用,適合多階段。掌握這個口訣,再結(jié)合上面的代碼案例,你在面試中談?wù)摗八膫€同步”時,就能做到既有深度又有廣度。
技術(shù)面試不是背題,而是展示你解決問題的思維過程。當(dāng)你能夠清晰區(qū)分這四個工具的底層機制和適用場景時,你就真正實現(xiàn)了從入門到精通的跨越。
你更常用哪種寫法?是在項目中偏愛synchronized的簡潔,還是喜歡ReentrantLock的靈活?或者你有過被CyclicBarrier坑過的經(jīng)歷?評論區(qū)交流,我們一起避坑。