魔鬼面試官必問:ConcurrentHashMap 線程安全嗎?
這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包
真愛,請設(shè)置“星標(biāo)”或點個“在看
來源:developer.aliyun.com/article/776568HashMap改為ConcurrentHashMap,就完美解決并發(fā)了呀?;蛘呤褂脤憰r復(fù)制的CopyOnWriteArrayList,性能更佳呀!技術(shù)言論雖然自由,但面對魔鬼面試官時,我們更在乎的是這些真的正確嗎?1 線程重用導(dǎo)致用戶信息錯亂
ThreadLocal緩存獲取到的用戶信息。ThreadLocal適用于變量在線程間隔離,而在方法或類間共享的場景。若用戶信息的獲取比較昂貴(比如從DB查詢),則在ThreadLocal中緩存比較合適。問題來了,為什么有時會出現(xiàn)用戶信息錯亂?1.1 案例

1.2 bug 重現(xiàn)
server.tomcat.max-threads=1
先讓用戶1請求接口,第一、第二次獲取到用戶ID分別是null和1,符合預(yù)期 
用戶2請求接口,bug復(fù)現(xiàn)!第一、第二次獲取到用戶ID分別是1和2,顯然第一次獲取到了用戶1的信息,因為Tomcat線程池重用了線程。兩次請求線程都是同一線程: http-nio-45678-exec-1。
Tomcat服務(wù)器下跑的業(yè)務(wù)代碼,本就運行在一個多線程環(huán)境(否則接口也不可能支持這么高的并發(fā)),并不能認(rèn)為沒有顯式開啟多線程就不會有線程安全問題 線程創(chuàng)建較昂貴,所以Web服務(wù)器會使用線程池處理請求,線程會被重用。使用類似ThreadLocal工具存放數(shù)據(jù)時,需注意在代碼運行完后,顯式清空設(shè)置的數(shù)據(jù)。
1.3 解決方案

1.4 ThreadLocalRandom 可將其實例設(shè)置到靜態(tài)變量,在多線程下重用嗎?
UNSAFE.putLong(t?=?Thread.currentThread(),?SEED,
r?=?UNSAFE.getLong(t,?SEED)?+?GAMMA);
UNSAFE.getLong(Thread.currentThread(),SEED);
2 ConcurrentHashMap真的安全嗎?
2.1 案例

訪問接口 
初始大小900符合預(yù)期,還需填充100個元素 worker13線程查詢到當(dāng)前需要填充的元素為49,還不是100的倍數(shù) 最后HashMap的總項目數(shù)是1549,也不符合填充滿1000的預(yù)期
2.2 bug 分析
使用不代表對其的多個操作之間的狀態(tài)一致,是沒有其他線程在操作它的。如果需要確保需要手動加鎖 諸如size、isEmpty和containsValue等聚合方法,在并發(fā)下可能會反映ConcurrentHashMap的中間狀態(tài)。因此在并發(fā)情況下,這些方法的返回值只能用作參考,而不能用于流程控制 。顯然,利用size方法計算差異值,是一個流程控制 諸如putAll這樣的聚合方法也不能確保原子性,在putAll的過程中去獲取數(shù)據(jù)可能會獲取到部分?jǐn)?shù)據(jù)
2.3 解決方案

只有一個線程查詢到需補100個元素,其他9個線程查詢到無需補,最后Map大小1000 
3 知己知彼,百戰(zhàn)百勝
3.1 案例
使用ConcurrentHashMap來統(tǒng)計,Key的范圍是10 使用最多10個并發(fā),循環(huán)操作1000萬次,每次操作累加隨機的Key 如果Key不存在的話,首次設(shè)置值為1。

判斷 讀取現(xiàn)在的累計值 +1 保存累加后值

ConcurrentHashMap的原子性方法computeIfAbsent做復(fù)合邏輯操作,判斷K是否存在V,若不存在,則把Lambda運行后結(jié)果存入Map作為V,即新創(chuàng)建一個LongAdder對象,最后返回V 因為computeIfAbsent返回的V是LongAdder,是個線程安全的累加器,可直接調(diào)用其increment累加。
3.2 性能測試
使用StopWatch測試兩段代碼的性能,最后的斷言判斷Map中元素的個數(shù)及所有V的和是否符合預(yù)期來校驗代碼正確性 
性能測試結(jié)果: 
3.3 computeIfAbsent高性能之道
static?final? ?boolean?casTabAt(Node []?tab,?int?i,
????????????????????????????????????Node?c,?Node ?v)?{
????return?U.compareAndSetObject(tab,?((long)i?<}
辨明 computeIfAbsent、putIfAbsent
當(dāng)Key存在的時候,如果Value獲取比較昂貴的話,putIfAbsent就白白浪費時間在獲取這個昂貴的Value上(這個點特別注意) Key不存在的時候,putIfAbsent返回null,小心空指針,而computeIfAbsent返回計算后的值 當(dāng)Key不存在的時候,putIfAbsent允許put null進(jìn)去,而computeIfAbsent不能,之后進(jìn)行containsKey查詢是有區(qū)別的(當(dāng)然了,此條針對HashMap,ConcurrentHashMap不允許put null value進(jìn)去)
3.4 CopyOnWriteArrayList 之殤
CopyOnWriteArrayList緩存大量數(shù)據(jù),而該業(yè)務(wù)場景下數(shù)據(jù)變化又很頻繁。CopyOnWriteArrayList雖然是一個線程安全版的ArrayList,但其每次修改數(shù)據(jù)時都會復(fù)制一份數(shù)據(jù)出來,所以只適用讀多寫少或無鎖讀場景。所以一旦使用CopyOnWriteArrayList,一定是因為場景適宜而非炫技。CopyOnWriteArrayList V.S 普通加鎖ArrayList讀寫性能
測試并發(fā)寫性能 
測試結(jié)果:高并發(fā)寫,CopyOnWriteArray比同步ArrayList慢百倍 
測試并發(fā)讀性能 
測試結(jié)果:高并發(fā)讀(100萬次get操作),CopyOnWriteArray比同步ArrayList快24倍 
4 總結(jié)
4.1 Don't !!!
不要只會用并發(fā)工具,而不熟悉線程原理 不要覺得用了并發(fā)工具,就怎么都線程安全 不熟悉并發(fā)工具的優(yōu)化本質(zhì),就難以發(fā)揮其真正性能 不要不結(jié)合當(dāng)前業(yè)務(wù)場景,就隨意選用并發(fā)工具,可能導(dǎo)致系統(tǒng)性能更差
4.2 Do !!!
認(rèn)真閱讀官方文檔,理解并發(fā)工具適用場景及其各API的用法,并自行測試驗證,最后再使用 并發(fā)bug本就不易復(fù)現(xiàn), 多自行進(jìn)行性能壓力測試
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?面試題?資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!
點擊??卡片,關(guān)注后回復(fù)【面試題】即可獲取
評論
圖片
表情

