
敘事框架面試題 → 標準答案驗證 → 三個翻車現場 → 邊界分析 → 升級版答案上篇講了線程池參數面試和生產場景的落差這篇我們來看另一道高頻面試題——ConcurrentHashMap。線程安全標準答案背得滾瓜爛熟但組合操作照樣翻車。面試題ConcurrentHashMap 為什么線程安全QConcurrentHashMap 和 HashMap 有什么區別為什么 ConcurrentHashMap 線程安全這道題幾乎每次 Java 面試都會出現標準答案也高度統一JDK 7分段鎖Segment 數組繼承 ReentrantLock默認 16 個 Segment鎖粒度粗JDK 8CAS synchronized 鎖單個 bin鏈表/紅黑樹頭節點鎖粒度降到數組元素級面試官聽到 JDK 7 vs JDK 8 的差異一般就滿意了。這道題從《Java 并發編程實戰》到各大公司的面試題庫答案幾乎一字不差。標準答案的隱含假設但如果你把標準答案拆開看它隱含了三個假設你只用單操作——只 put 一個 key、只 get 一個 key、只 remove 一個 key你來決定 JDK 版本——JDK 8 默認JDK 7 是歷史你只關心容器本身——不關心調用方怎么編排這些操作三個假設在面試中都被認為是理所當然的。直到生產環境把它們一個個擊穿。生產事故線程安全容器也翻車事故一“賣了 5 件只扣了 3 件”陳姐維護的庫存服務核心邏輯只有兩行intstockcache.get(key);cache.put(key,stock-1);100 個線程同時扣庫存。上線第一周正常——并發量低。第二周大促流量進來庫存對不上了賬面顯示還有 3 件實際賣了 5 件。這不是 ConcurrentHashMap 線程不安全——是get和put各自線程安全但它們之間沒有原子性。兩個線程同時讀到stock 3各自減 1 寫回2——賣了兩件只扣了一件。兩個get()之間沒有 happens-before 關系所以讀到了相同值。面試的標準答案是對的“put 和 get 是線程安全的。”——但你的業務代碼不是map.put(key, value)你的代碼是map.put(key, map.get(key) - 1)。事故二CPU 100%所有線程卡在 get() 上如果事故一還算溫和數據錯但服務還在跑事故二是直接宕機。某網關服務JDK 7上線一個月沒出過問題。某天 CPU 突然 100%jstack顯示所有線程全部停在ConcurrentHashMap.get()上。排查發現服務需要定期刷新緩存大量并發 put 觸發了 ConcurrentHashMap 的 resize。JDK 7 的 resize 使用頭插法遷移——多線程同時 resize鏈表形成環get()遍歷這個環永遠停不下來。JDK 8 換用了 ForwardingNode 做無鎖遷移不存在此問題。但問題在于你的依賴 jar 可能還在用 JDK 7 編譯的版本。Gateway 本身是 JDK 8但引入的某個中間件客戶端依賴了 JDK 7 版本的 ConcurrentHashMap 用法。面試的標準答案也沒錯——JDK 8 確實沒有這個問題。但它沒告訴你你的依賴可能悄悄拖著一個 JDK 7。事故三批量寫入后 size 對不上第三個事故最隱蔽——數據沒丟、服務沒掛但報表對不上。批處理任務批量寫入 20 萬條數據寫入完成后讀size()cache.putAll(batch);log.info(寫入完成總數{},cache.size());// 輸出156,842期望 200,000實際 156,842。差了 43,158 條。不是 bug——size()在 JDK 8 中使用CounterCell[]baseCount做近似計數。高并發寫入時size()返回的是能快速拿到的最新近似值不是精確的事務計數。但業務方把它當精確值用了下游系統按這個數做結算差了 4 萬多。面試的標準答案繼續成立——“ConcurrentHashMap 線程安全”。但線程安全不意味著size()是實時精確的。為什么標準答案不夠標準答案對在哪?單操作原子性put(k, v)、get(k)、remove(k)各自是線程安全的——面試說的這個完全正確?弱一致性迭代迭代器不拋ConcurrentModificationException——對面試說的也正確?JDK 8 的演進方向對從 Segment 到 CAS synchronized粒度更細、并發度更高——正確標準答案漏了哪漏了什么面試場景生產場景組合操作只問單操作是否安全業務代碼全是組合getput、containsKeyput、putAll版本差異默認 JDK 8依賴 jar 可能用 JDK 7 編譯間接拖入舊版本size() 語義“size 返回元素數量”近似計數高并發下不準修復手段不討論compute / putIfAbsent / mappingCount / 外部鎖ConcurrentHashMap 安全性的三層邊界安全級別1單操作原子性 ? ← 面試只問到這 put(k,v)/ get(k)/ remove(k)各自線程安全 安全級別2弱一致性迭代 ? ← 面試偶爾問到 迭代器不拋 ConcurrentModificationException 但不保證看到全部最新寫入 安全級別3組合操作原子性 ? ← 生產踩坑全在這 get put、containsKey put、putAll、size()需要外部同步或使用 compute()/ merge()面試升級版答案第一層基礎答案及格線ConcurrentHashMap 用 CAS synchronized 保證線程安全。JDK 7 用分段鎖JDK 8 鎖粒度降到 bin 級別。大多數候選人到此為止。能答出 JDK 版本差異的算合格。第二層推導邊界拉開差距但’線程安全’只保證單操作的原子性。組合操作getput、containsKeyput沒有跨操作保證。一個線程 put 完另一個線程 get 能讀到——但一個線程 get 然后 put這兩個操作之間的窗口另一個線程也能進來。真正的安全邊界面試不會考三層——單操作 ?、弱一致性迭代 ?、組合操作 ?。這一步把背結論變成了講邊界。面試官會意識到你不只是刷了八股。第三層生產案例面試加分項結合真實案例講我之前維護過一個庫存服務用 ConcurrentHashMap 做緩存也是標準的 get put 扣庫存——上線前壓測正常大促流量進來庫存對不上。排查發現是 read-modify-write 丟失更新。修復方案把裸 put 改成 compute() 或 merge()保證 read 和 write 的原子性。同時補充了 JDK 版本檢查——某個依賴 jar 的 ConcurrentHashMap 用法從 JDK 7 編譯過來的修改了依賴版本才解決。同步展示三個事故的修復方案對比第四層監控驗證真正的高階面試官可能追問“修復完你就放心了”不放心。加了三道防線代碼審查grep 檢查ConcurrentHashMap.*\.get(.*put模式——所有 RMW 都要改成 computeJDK 版本審計mvn dependency:tree檢查所有傳遞依賴的 JDK 版本數據校驗重要業務加對賬——ConcurrentHashMap 的 size 不用來做業務判斷用 mappingCount 做參考生產中這么用安全操作速查場景? 面試八股寫法? 生產正確用法原子增減map.put(k, map.get(k) 1)map.compute(k, (k,v) - vnull ? 1 : v1)不存在時寫入if (!map.containsKey(k)) map.put(k, v)map.putIfAbsent(k, v)批量寫入后計數map.putAll(batch); map.size()map.putAll(batch); long n map.mappingCount()遍歷時刪除for (Entry e: map.entrySet()) map.remove(...)map.forEach(2, (k,v) - { map.remove(k); })?compute內拋異常會刪除該 key——短操作用 compute長業務用外部鎖。grep 檢查你的項目# 檢查 read-modify-write 模式最常翻車grep-rnConcurrentHashMap.*\.get(src/|grep-Eput|remove# 檢查裸 check-then-actgrep-rncontainsKey.*ConcurrentHashMapsrc/# 檢查傳遞依賴的 JDK 版本mvn dependency:tree|grepconcurrent# 檢查 size() 做業務判斷grep-rnConcurrentHashMap.*\.size()src/|grep-vlog\|print“面試題的標準答案只是地圖——只有到生產里走一次才知道地圖漏了哪條路。”下篇我們聊強/軟/弱/虛引用——面試全能背生產 OOM 還是不會查。