敢說清嗎?ConcurrentHashMap.get 要不要加鎖?
敢说清吗?ConcurrentHashMap.get 要不要加锁?
说起ConcurrentHashMap,那是我和同事们年年绕不开的话题之一。别问,问就是“线程安全”,嘴上说着安全,心里还是咯噔一下——真安全吗?尤其是get方法,到底要不要再套一层锁?前几年刚入职那会儿,这个问题让我真挠了不少头皮。
说起来,那个傍晚我正一边啃外卖鸡腿,一边写着多线程缓存。隔壁的后端兄弟突然发消息:“哥们,这个Map外面你加不加锁?”我嘴快:“ConcurrentHashMap还用锁?不是写着线程安全嘛!”
他不信,还专门贴过来一段代码建议我double check一下。我这面子下不来,就决定今晚自己搞清楚——到底get要不要加锁。
探寻幕后:ConcurrentHashMap.get 到底干了啥
我撸起袖子,把源码点开撸了一遍。
核心就这段:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 哈希分桶,然后链表遍历查找元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & hash)) != null) {
if ((eh = e.hash) == hash &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
// 省略了后面for循环遍历链表找key
// ...
}
return null;
}
看上去,get根本没加任何synchronized。啥锁也没见着。
那是不是这货就“光膀子”冲进多线程沙场也毫发无伤?
踩坑瞬间
我开始信心满满,顺手写了个测试小程序,几个线程一起撸get操作。
一开始,一切正常:
- 同时读,没报错,值对得上。
- 写完再读,也没乱套,看起来稳如老狗。
突然,老板让这个缓存支持“懒加载”——没查到就初始化。
我第一反应,聪明地:
V value = map.get(key);
if (value == null) {
value = loadFromDB(key);
map.put(key, value);
}
完美吧?
结果真·并发一上——同一个key被loadFromDB搞了N次,性能滑铁卢,我的脸也跟着绿了。
原来,get没有加锁确实快,但在并发“读-改-写”这个场景下,
多个线程可能几乎同时get到null,然后各自加载各自的。这种坑,
真的只有踩了,你才记得深刻。
经验启示
所以,总结一下血泪教训:
- 纯get(只读):不用加锁,ConcurrentHashMap保证可见性和一致性,多线程安全地读。
- 读-改-写(比如懒加载、缓存穿透):要加锁,或者用更安全的原子操作(如
computeIfAbsent
)。 - 出现如下需求时,记得报警:
- 多线程下判断key是否存在再put
- 关联两个get/put操作的逻辑。
可以用的手法有:
map.computeIfAbsent(key, k -> loadFromDB(k))
(内部帮你锁好)- 或者,搞个小锁(line lock、synchronized、ReentrantLock),别太信任平白无故的“不加锁更快”神话。
边啃鸡腿边收尾
回头看,ConcurrentHashMap的get确实不用加锁,
可是你的“业务流程”是不是纯读,那得用脑袋琢磨清楚。如果弄成“读-改-写”,别傻乐着,早点用computeIfAbsent吧——省事省心,老板夸你,自己晚上也睡得着觉。
反正这次之后,我微信群昵称改成了“懂得加锁的懒加载专家”。
谁再问我这个问题?就把这篇文章甩过去,省得啰嗦。
兄弟们,咱们下次再见!
共同學(xué)習(xí),寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章