)
Java 并發(fā)面試題匯總Java 并發(fā)是 Java 后端面試中的高頻考點尤其是synchronized、volatile、CAS、AQS、ReentrantLock、線程池、ThreadLocal等。本文按照“線程基礎 → Java 內(nèi)存模型 → synchronized → volatile → CAS → AQS → Lock → 線程池 → ThreadLocal → 并發(fā)容器”的順序進行整理方便面試前快速復習。一、線程基礎1. 什么是進程什么是線程進程是操作系統(tǒng)進行資源分配的基本單位一個進程擁有獨立的內(nèi)存空間。線程是操作系統(tǒng)進行 CPU 調度的基本單位一個進程可以包含多個線程這些線程共享進程的內(nèi)存資源。例如JVM 進程 │ ├── 主線程 ├── GC 線程 ├── JIT 編譯線程 └── 其他業(yè)務線程面試回答進程是資源分配的基本單位線程是 CPU 調度的基本單位。同一個進程中的多個線程共享堆、方法區(qū)等資源但每個線程擁有自己的程序計數(shù)器、虛擬機棧和本地方法棧。2. 為什么要使用多線程主要有三個原因① 提高 CPU 利用率如果一個線程因為 IO 操作阻塞CPU 可以切換去執(zhí)行其他線程。② 提高程序響應速度多個任務可以并發(fā)執(zhí)行。③ 提高系統(tǒng)吞吐量例如 Web 服務中可以使用線程池同時處理多個用戶請求。3. 創(chuàng)建線程有哪些方式常見方式方式一繼承 ThreadclassMyThreadextendsThread{Overridepublicvoidrun(){System.out.println(執(zhí)行任務);}}方式二實現(xiàn) RunnableRunnabletask()-{System.out.println(執(zhí)行任務);};newThread(task).start();方式三實現(xiàn) CallableCallableIntegertask()-100;FutureTaskIntegerfutureTasknewFutureTask(task);newThread(futureTask).start();方式四使用線程池實際開發(fā)中更推薦線程池ExecutorServiceexecutorExecutors.newFixedThreadPool(10);executor.submit(()-{System.out.println(執(zhí)行任務);});二、線程的生命周期Java 中線程主要有以下狀態(tài)NEW │ │ start() ↓ RUNNABLE │ ├── BLOCKED ├── WAITING └── TIMED_WAITING │ ↓ TERMINATED4. NEW線程對象創(chuàng)建完成但是還沒有調用start()。ThreadthreadnewThread();5. RUNNABLE調用start()后進入RUNNABLE狀態(tài)。需要注意Java 中的 RUNNABLE 包含操作系統(tǒng)層面的“就緒”和“運行”兩種狀態(tài)。6. BLOCKED線程等待獲取synchronized鎖時如果鎖已經(jīng)被其他線程占用就會進入 BLOCKED。7. WAITING線程主動等待其他線程通知。例如ObjectlocknewObject();synchronized(lock){lock.wait();}8. TIMED_WAITING帶時間限制的等待。例如Thread.sleep(1000);或者lock.wait(1000);三、并發(fā)編程存在什么問題并發(fā)編程最核心的三個問題可見性、原子性、有序性9. 什么是可見性一個線程修改共享變量之后其他線程能夠立即看到修改結果。例如booleanflagfalse;ThreadA:flagtrue;ThreadB:while(!flag){}如果沒有正確的內(nèi)存同步機制線程 B 可能一直讀取到舊值。10. 什么是原子性一個操作要么全部執(zhí)行要么全部不執(zhí)行中間不能被其他線程打斷。例如count;看起來是一條語句實際上可以拆成讀取 count ↓ count 1 ↓ 寫回 count如果兩個線程同時執(zhí)行就可能發(fā)生數(shù)據(jù)覆蓋。11. 什么是有序性程序執(zhí)行過程中編譯器、JIT 和 CPU 為了優(yōu)化性能可能對指令進行重排序。在單線程環(huán)境下重排序通常不會影響最終結果。但是在多線程環(huán)境下如果沒有正確的同步機制就可能產(chǎn)生問題。四、synchronized12. synchronized 是什么synchronized是 Java 提供的內(nèi)置鎖機制可以保證多個線程對共享資源的互斥訪問。同時可以保證原子性可見性有序性例如publicsynchronizedvoidadd(){count;}13. synchronized 鎖的是什么synchronized本質上是對對象監(jiān)視器 Monitor進行加鎖。例如synchronized(obj){// 臨界區(qū)}鎖住的是obj對象。14. synchronized 修飾實例方法鎖的是誰publicsynchronizedvoidmethod(){}等價于publicvoidmethod(){synchronized(this){}}所以鎖的是當前對象this15. synchronized 修飾靜態(tài)方法鎖的是誰publicstaticsynchronizedvoidmethod(){}鎖的是當前類的 Class 對象。例如synchronized(MyClass.class){}16. synchronized 的鎖升級過程早期 JVM 中經(jīng)常介紹無鎖 ↓ 偏向鎖 ↓ 輕量級鎖 ↓ 重量級鎖不過需要注意JDK 15 開始偏向鎖默認被禁用JDK 18 移除了偏向鎖相關實現(xiàn)。因此面試時可以了解歷史上的鎖升級機制但不要把“偏向鎖一定存在”當成當前 JVM 的絕對事實。傳統(tǒng)鎖升級主要是為了在不同競爭程度下用不同成本的鎖實現(xiàn)提高性能。五、volatile17. volatile 是什么volatile是 Java 提供的輕量級同步機制可以保證可見性 禁止特定類型的指令重排序例如privatevolatilebooleanrunningtrue;一個線程修改runningfalse;其他線程能夠及時看到最新值。18. volatile 能保證原子性嗎不能。例如volatileintcount0;count;依然不是原子操作。因為讀取 count ↓ count 1 ↓ 寫回 count多個線程依然可能同時讀取到相同的值。19. synchronized 和 volatile 有什么區(qū)別對比synchronizedvolatile原子性??可見性??有序性?部分保證阻塞可能阻塞不會因為讀寫本身產(chǎn)生鎖阻塞使用場景臨界區(qū)、復合操作狀態(tài)標志、簡單共享變量一句話volatile適合解決共享變量的可見性問題synchronized更適合解決需要互斥執(zhí)行的臨界區(qū)問題。六、Java 內(nèi)存模型 JMM20. 什么是 JMMJMM即 Java Memory ModelJava 內(nèi)存模型。它不是簡單指 JVM 中的某一塊物理內(nèi)存而是一套規(guī)范用于解決多線程環(huán)境下共享變量的可見性、原子性和有序性問題。線程擁有自己的工作內(nèi)存主內(nèi)存 / | \ / | \ 線程 A 線程 B 線程 C 工作內(nèi)存 工作內(nèi)存 工作內(nèi)存線程對共享變量的操作需要通過主內(nèi)存進行同步。七、happens-before21. 什么是 happens-beforehappens-before 是 JMM 中用于描述操作之間可見性關系的規(guī)則。如果A happens-before B那么意味著A 操作的結果對 B 操作可見。常見規(guī)則① 程序次序規(guī)則同一個線程中inta10;intba;前面的操作 happens-before 后面的操作。② 鎖規(guī)則解鎖操作 happens-before 后續(xù)對同一個鎖的加鎖操作。③ volatile 規(guī)則對 volatile 變量的寫操作 happens-before 后續(xù)對該變量的讀操作。④ 線程啟動規(guī)則Thread.start()happens-before 新線程中的操作。⑤ 線程終止規(guī)則線程中的所有操作 happens-before 其他線程檢測到該線程已經(jīng)終止。例如thread.join();八、CAS22. 什么是 CASCASCompare And Swap比較并交換。它是一種樂觀鎖思想。CAS 包含三個值內(nèi)存地址 舊值 新值執(zhí)行邏輯當前值 預期舊值 │ ┌────┴────┐ │ │ 是 否 │ │ 修改新值 修改失敗23. CAS 有什么優(yōu)點CAS 不需要像傳統(tǒng)互斥鎖一樣讓線程阻塞等待。因此在競爭不激烈的情況下性能較好。Java 中很多原子類底層都會使用 CAS。例如AtomicIntegercountnewAtomicInteger();count.incrementAndGet();24. CAS 有什么問題主要有兩個① ABA 問題假設線程 A讀取 A 線程 BA → B 線程 BB → A 線程 A發(fā)現(xiàn)還是 A于是 CAS 成功線程 A 無法感知中間發(fā)生過變化。解決方式使用帶版本號的 CAS。例如AtomicStampedReference② 自旋開銷CAS 失敗后可能不斷重試CAS失敗 ↓ 重新讀取 ↓ CAS ↓ 失敗 ↓ 繼續(xù)重試如果競爭非常激烈會消耗 CPU。九、Atomic 原子類25. AtomicInteger 是什么AtomicInteger是 Java 并發(fā)包提供的原子操作類。例如AtomicIntegercountnewAtomicInteger();count.incrementAndGet();相比count;可以保證自增操作的原子性。26. AtomicInteger 為什么線程安全底層主要依賴CAS volatile例如讀取當前值 ↓ 計算新值 ↓ CAS 更新 ↓ 失敗則重試十、AQS27. 什么是 AQSAQSAbstractQueuedSynchronizer抽象隊列同步器。它是 Java 并發(fā)包中非常核心的基礎組件。很多并發(fā)工具都是基于 AQS 實現(xiàn)的例如ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch28. AQS 的核心思想是什么AQS 核心可以理解成一個 state 一個等待隊列例如AQS │ ┌─────┴─────┐ │ │ state CLH隊列 │ ┌──────┼──────┐ ↓ ↓ ↓ 線程A 線程B 線程Cstate用來表示同步狀態(tài)。當線程獲取資源失敗后就進入等待隊列。29. AQS 為什么需要隊列如果線程獲取鎖失敗不能讓所有線程一直 CAS 自旋否則會浪費大量 CPU。所以 AQS 會獲取資源失敗 ↓ 進入等待隊列 ↓ 阻塞線程 ↓ 資源釋放 ↓ 喚醒后繼節(jié)點這樣可以降低 CPU 消耗。十一、ReentrantLock30. ReentrantLock 是什么ReentrantLock是 Java 提供的可重入鎖。例如ReentrantLocklocknewReentrantLock();lock.lock();try{// 臨界區(qū)}finally{lock.unlock();}31. ReentrantLock 和 synchronized 有什么區(qū)別對比synchronizedReentrantLock使用方式JVM 內(nèi)置Java API可重入??公平鎖不支持顯式設置支持嘗試獲取鎖不方便tryLock()可中斷支持一定形式lockInterruptibly()條件隊列wait/notifyCondition自動釋放??需要手動 unlock面試時可以這樣回答兩者都可以實現(xiàn)互斥和可重入但 ReentrantLock 提供了更豐富的功能例如公平鎖、嘗試獲取鎖、可中斷獲取鎖以及多個 Condition 條件隊列。synchronized 使用起來更加簡單正常情況下優(yōu)先使用 synchronized 即可。十二、ReentrantLock 為什么可重入例如lock.lock();lock.lock();try{// ...}finally{lock.unlock();lock.unlock();}同一個線程可以多次獲取同一把鎖。內(nèi)部會記錄持有線程 當前線程 state 2每獲取一次state每釋放一次state--直到state 0鎖才真正釋放。十三、Condition32. Condition 是什么Condition可以理解為ReentrantLock 版本的 wait/notify。例如Conditionconditionlock.newCondition();lock.lock();try{condition.await();}finally{lock.unlock();}其他線程lock.lock();try{condition.signal();}finally{lock.unlock();}十四、線程池33. 為什么要使用線程池線程池主要解決三個問題① 降低線程創(chuàng)建和銷毀的開銷線程可以重復使用。② 控制線程數(shù)量避免創(chuàng)建大量線程導致 CPU 和內(nèi)存資源耗盡。③ 統(tǒng)一管理線程可以統(tǒng)一進行任務提交線程管理任務排隊拒絕策略十五、線程池核心參數(shù)JavaThreadPoolExecutor最重要的參數(shù)ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);分別表示參數(shù)含義corePoolSize核心線程數(shù)maximumPoolSize最大線程數(shù)keepAliveTime非核心線程空閑存活時間workQueue任務等待隊列threadFactory創(chuàng)建線程的工廠handler拒絕策略十六、線程池執(zhí)行流程這是線程池面試中非常重要的一道題。假設核心線程數(shù) 2 最大線程數(shù) 4 隊列容量 10提交任務后任務提交 ↓ 核心線程是否未滿 ↓ 是 → 創(chuàng)建核心線程執(zhí)行 ↓ 否 ↓ 任務加入阻塞隊列 ↓ 隊列滿了嗎 ↓ 否 → 等待 ↓ 是 ↓ 線程數(shù) 最大線程數(shù) ↓ 是 → 創(chuàng)建非核心線程 ↓ 否 ↓ 執(zhí)行拒絕策略面試時一定要能把這個流程講出來。十七、線程池為什么不推薦 Executors例如Executors.newFixedThreadPool(10);看起來方便但是實際開發(fā)中通常不推薦直接使用。原因是FixedThreadPool底層使用無界隊列LinkedBlockingQueue任務過多時可能導致大量任務堆積最終造成 OOM。CachedThreadPool線程數(shù)量理論上可以無限增長。大量任務提交時可能創(chuàng)建大量線程。所以實際開發(fā)中一般直接使用ThreadPoolExecutor明確指定核心線程數(shù)最大線程數(shù)隊列容量拒絕策略十八、線程池拒絕策略線程池無法繼續(xù)接收任務時會執(zhí)行拒絕策略。JDK 提供四種常見策略AbortPolicy直接拋出異常。RejectedExecutionExceptionCallerRunsPolicy由提交任務的線程自己執(zhí)行任務。DiscardPolicy直接丟棄任務。DiscardOldestPolicy丟棄隊列中最舊的任務然后重新提交。十九、ThreadLocal34. 什么是 ThreadLocalThreadLocal可以為每個線程提供獨立的變量副本。例如ThreadLocalStringthreadLocalnewThreadLocal();threadLocal.set(hello);StringvaluethreadLocal.get();不同線程獲取到的是不同的數(shù)據(jù)。35. ThreadLocal 的底層原理可以簡單理解為Thread │ └── ThreadLocalMap │ ├── ThreadLocal → value ├── ThreadLocal → value └── ThreadLocal → value注意ThreadLocalMap 是 Thread 對象內(nèi)部的一部分每個線程都有自己的 ThreadLocalMap。二十、ThreadLocal 為什么會內(nèi)存泄漏這是非常高頻的問題。ThreadLocalMap 中Entry ├── KeyThreadLocal 的弱引用 └── Value強引用如果 ThreadLocal 對象沒有其他強引用ThreadLocal ↓ 被 GC 回收 ↓ Entry 的 Key null但是Thread ↓ ThreadLocalMap ↓ Entry ↓ ValueValue 仍然可能存在強引用。如果線程長期存活例如線程池中的工作線程就可能導致 Value 長時間無法回收。因此使用 ThreadLocal 后應該及時try{threadLocal.set(value);}finally{threadLocal.remove();}二十一、并發(fā)容器Java 提供了一系列線程安全的并發(fā)容器。常見的有ConcurrentHashMap CopyOnWriteArrayList ConcurrentLinkedQueue BlockingQueue二十二、ConcurrentHashMap36. 為什么需要 ConcurrentHashMap普通 HashMap 在多線程環(huán)境下可能出現(xiàn)數(shù)據(jù)覆蓋數(shù)據(jù)不一致并發(fā)修改問題因此需要線程安全的 Map。37. ConcurrentHashMap 和 Hashtable 有什么區(qū)別Hashtable很多方法直接使用 synchronized。鎖粒度比較大。而ConcurrentHashMap采用更加細粒度的并發(fā)控制方式因此并發(fā)性能更好。38. JDK 1.7 的 ConcurrentHashMapJDK 1.7 使用Segment HashEntry整體結構ConcurrentHashMap │ ┌──────┼──────┐ Segment Segment Segment │ HashEntry...不同 Segment 可以同時執(zhí)行操作。39. JDK 1.8 的 ConcurrentHashMapJDK 1.8 去掉了 Segment整體結構更接近Node[] │ ├── Node ├── Node └── TreeBin主要通過CAS synchronized實現(xiàn)并發(fā)控制。面試可以簡單總結JDK 1.7 的 ConcurrentHashMap 采用 Segment 分段鎖JDK 1.8 去掉 Segment采用 CAS synchronized 對桶進行更細粒度的控制并在鏈表過長時轉成紅黑樹。二十三、CopyOnWriteArrayList40. 什么是 CopyOnWriteArrayListCopyOnWriteArrayList 是一種適合讀多寫少場景的線程安全 List。寫操作時會復制一份數(shù)組原數(shù)組 [ A B C ] ↓ 寫操作 新數(shù)組 [ A B C D ]然后再替換原來的數(shù)組。41. CopyOnWriteArrayList 有什么缺點① 寫操作成本高每次寫都需要復制數(shù)組。② 占用額外內(nèi)存修改期間會存在新舊兩份數(shù)組。③ 不適合大量寫操作所以它比較適合讀多寫少的場景。二十四、BlockingQueue42. 什么是 BlockingQueue阻塞隊列。當隊列滿時生產(chǎn)者阻塞。當隊列為空時消費者阻塞。典型應用生產(chǎn)者-消費者模型。例如線程池中的任務隊列本質上也是一種生產(chǎn)者消費者思想。二十五、CountDownLatch43. CountDownLatch 是什么CountDownLatch 可以讓一個或多個線程等待其他線程完成任務。例如CountDownLatchlatchnewCountDownLatch(3);三個線程分別執(zhí)行l(wèi)atch.countDown();主線程latch.await();直到計數(shù)器3 → 2 → 1 → 0主線程才繼續(xù)執(zhí)行。二十六、Semaphore44. Semaphore 是什么Semaphore 用于控制同時訪問某個資源的線程數(shù)量。例如限制最多 3 個線程訪問數(shù)據(jù)庫SemaphoresemaphorenewSemaphore(3);線程獲取許可semaphore.acquire();使用完成semaphore.release();可以把它理解成一個“許可證池”。二十七、死鎖45. 什么是死鎖兩個或多個線程互相等待對方持有的資源導致所有線程都無法繼續(xù)執(zhí)行。例如線程 A 持有鎖1 等待鎖2 線程 B 持有鎖2 等待鎖1形成A → 等待 B ↑ ↓ └───────┘二十八、死鎖產(chǎn)生的四個必要條件經(jīng)典死鎖條件① 互斥資源一次只能被一個線程使用。② 持有并等待線程已經(jīng)持有資源同時繼續(xù)等待其他資源。③ 不可剝奪線程已經(jīng)獲得的資源不能被強制搶走。④ 循環(huán)等待多個線程形成循環(huán)等待關系。只要破壞其中一個條件就可以避免死鎖。二十九、如何排查死鎖可以使用jstack例如jstackpidJVM 會輸出線程之間的鎖競爭情況。另外也可以使用jconsole或者VisualVM輔助分析。三十、sleep 和 wait 有什么區(qū)別這是面試中的經(jīng)典題。對比sleepwait所屬類ThreadObject是否釋放鎖??是否需要 synchronized不需要需要喚醒方式時間到自動恢復notify/notifyAll 或超時使用場景線程休眠線程間通信例如synchronized(lock){lock.wait();}調用wait()后當前線程釋放鎖并進入等待狀態(tài)。三十一、notify 和 notifyAll 有什么區(qū)別notify()隨機喚醒一個等待線程。notifyAll()喚醒所有等待線程。實際開發(fā)中如果無法明確判斷應該喚醒哪個線程通常更安全的是notifyAll();三十二、并發(fā)編程面試高頻總結面試 Java 并發(fā)時可以重點掌握下面這張知識圖Java 并發(fā) │ ├── 線程基礎 │ ├── 線程狀態(tài) │ ├── sleep / wait │ └── notify / notifyAll │ ├── JMM │ ├── 可見性 │ ├── 原子性 │ ├── 有序性 │ └── happens-before │ ├── synchronized │ ├── Monitor │ ├── 可重入 │ └── 鎖機制 │ ├── volatile │ ├── 可見性 │ └── 禁止特定重排序 │ ├── CAS │ ├── Atomic │ ├── ABA │ └── 自旋 │ ├── AQS │ ├── state │ ├── 等待隊列 │ └── ReentrantLock │ ├── 線程池 │ ├── 核心參數(shù) │ ├── 執(zhí)行流程 │ └── 拒絕策略 │ ├── ThreadLocal │ ├── ThreadLocalMap │ └── 內(nèi)存泄漏 │ ├── 并發(fā)容器 │ ├── ConcurrentHashMap │ ├── CopyOnWriteArrayList │ └── BlockingQueue │ └── 并發(fā)工具類 ├── CountDownLatch ├── Semaphore └── CyclicBarrier三十三、面試前重點背誦清單如果時間比較緊不需要平均分配時間優(yōu)先掌握下面這些。第一梯隊必須掌握synchronized 原理synchronized 和 volatile 的區(qū)別volatile 為什么不能保證原子性JMMhappens-beforeCASABA 問題AQSReentrantLock線程池核心參數(shù)線程池執(zhí)行流程線程池拒絕策略ThreadLocal 原理ThreadLocal 內(nèi)存泄漏ConcurrentHashMap 1.7 和 1.8 的區(qū)別死鎖第二梯隊需要理解sleep 和 wait 區(qū)別notify 和 notifyAllCountDownLatchSemaphoreCopyOnWriteArrayListBlockingQueueAtomic 原子類第三梯隊了解即可CyclicBarrierPhaserExchangerForkJoinPoolCompletableFuture三十四、最后總結Java 并發(fā)這部分看起來知識點很多但實際上可以圍繞幾個核心問題展開多個線程同時操作共享數(shù)據(jù)時如何保證數(shù)據(jù)正確于是就產(chǎn)生了可見性 ↓ volatile / synchronized / JMM 原子性 ↓ synchronized / Lock / CAS / Atomic 有序性 ↓ volatile / happens-before 線程協(xié)作 ↓ wait / notify / Condition 線程競爭 ↓ synchronized / AQS / ReentrantLock 任務管理 ↓ ThreadPoolExecutor 線程隔離 ↓ ThreadLocal 并發(fā)容器 ↓ ConcurrentHashMap / CopyOnWriteArrayList真正面試的時候不要只背定義。例如面試官問“為什么 volatile 不能保證 count 的線程安全”不要只回答“因為 volatile 不能保證原子性?!备玫幕卮鸱绞绞莄ount實際上包含讀取、修改、寫回三個步驟即使 count 使用 volatile 保證了可見性多個線程仍然可能同時讀取到相同的舊值然后分別進行加一并寫回導致其中一次修改被覆蓋。因此 volatile 只能保證單次讀寫的可見性不能保證這種復合操作的原子性。如果需要保證自增操作的線程安全可以使用 AtomicInteger 的 CAS或者使用 synchronized / Lock。這才是面試中真正有區(qū)分度的回答。Java 并發(fā)學習路線如果按照后端面試的順序學習可以記成線程基礎 ↓ JMM ↓ synchronized ↓ volatile ↓ CAS ↓ Atomic ↓ AQS ↓ ReentrantLock ↓ 線程池 ↓ ThreadLocal ↓ ConcurrentHashMap ↓ 其他并發(fā)工具類 ↓ 源碼 實戰(zhàn)其中最重要的是JMM → synchronized / volatile → CAS → AQS → 線程池 → ThreadLocal → ConcurrentHashMap把這一條主線真正理解之后再去看源碼會比單純背八股輕松很多。