HashMap为什么线程不安全? ConcurrentHashMap如何保证线程安全? AQS如何实现锁的获取与释放?用男女关系进行解释,一看就懂
HashMap在多线程下容易出问题,主要是因为它的核心操作不是“原子”的,就像一对情侣同时往一个共享日记本里写秘密,很容易互相干扰。
💔 HashMap的线程不安全“情侣矛盾”
数据覆盖:当你们(线程A和B)同时往同一个位置写东西,如果时机不对,后写的人可能会把先写的人的内容给擦掉覆盖了。
扩容混乱:当日记本(HashMap)需要换更大的本子时(扩容),这个过程很复杂。如果这时还有人不停地往里写新内容,很容易导致一些记录丢失,或者本子里的索引(链表)打结,形成死循环。
计数出错:日记本里记录总数(size)的统计也不是一步完成的,你们同时写,最后总数可能比实际少记了。
🤝 ConcurrentHashMap的“默契协作”
ConcurrentHashMap解决了上面的问题,它采用了更聪明的分工方式,就像情侣间约定好分区管理日记本:
分段锁(JDK7):把日记本分成很多个小格子,你们各自负责不同的格子,写自己格子里的内容时互不打扰,大大减少了冲突和等待。
CAS+synchronized(JDK8):这种方式更灵活。对于空位置,尝试用“快速打招呼”(CAS)的方式直接写入;如果位置有人(发生哈希冲突),才用“小范围商量”(synchronized锁住链表头或树根)来协调。 这就像看到想写的地方没人就直接写,有人才需要简单沟通一下。
🔑 AQS的“排队领证”机制
AQS(AbstractQueuedSynchronizer)是Java并发包中锁(如ReentrantLock)实现的核心,可以把它想象成民政局婚姻登记处的叫号系统,核心是一个“排队领证”的机制:
获取锁(尝试领证):当一对情侣(线程)想来“领证”(获取锁)时,会先看一下当前窗口状态(state变量)。如果没人(state为0),他们就直接办理;如果有人在办(state不为0),他们就需要取号排队,进入等待队列。这个排队系统(CLH队列)保证了先来的先服务。
释放锁(办完离开):当这对情侣办完手续,就会离开窗口(释放锁),同时系统(AQS)会通知排队中的下一对情侣(线程)来办理。
💡 给你的“恋爱”建议
如果你的“感情生活”(应用场景)中,“写日记”(修改Map)的操作比较多,那么使用ConcurrentHashMap会是更明智的选择,它能有效避免你们之间的“争吵”(数据不一致)。
对于更复杂的“感情约定”(同步控制),AQS提供的“排队机制”是一个非常强大和灵活的基础工具。
用男女关系解释AQS的排队领证机制
想象一下,AQS就是民政局婚姻登记处的核心叫号排队系统,而你们一对对情侣(线程)就是来办理结婚登记(获取锁)的。
1. 核心资源:结婚窗口 (The State)
民政局只有一个真正的结婚窗口(或者说,同一时间只允许一对情侣办理)。这个窗口的“空闲/忙碌”状态,就是AQS核心的 state 变量。
state = 0:窗口空闲,可以办理。
state = 1:窗口忙碌,正在为一对情侣服务。
2. 获取锁:尝试领证 (Acquiring the Lock)
现在,你和你的伴侣(线程A)来到民政局,想要领证。
第一步:直接尝试 (CAS操作)
你们不会直接冲上去,而是先彬彬有礼地看一眼窗口。发现窗口是空闲的 (state=0)。你们会快速而礼貌地(通过一次原子性的CAS操作)尝试把状态从“0”改成“1”,表示“这个窗口我们占了”。
成功:恭喜!你们立刻开始办理手续(线程A成功获取锁,进入临界区执行代码)。整个过程非常迅速,没有排队。
第二步:需要排队 (入队操作)
如果你们来的时候,窗口已经有人了 (state=1),或者在你尝试CAS的时候,另一对情侣(线程B)手更快,抢先占用了窗口(CAS失败)。
这时,你们不能傻站着干等。叫号系统(AQS)会给你们一个排队号,并请你们到等候区坐着(将线程A封装成一个Node节点,加入CLH队列的尾部)。
等候区的规矩:在排队时,你们不能不停地去问“到我们了吗?”,这样很烦人(避免了大量无用的CPU自旋)。正确的做法是:你们会安静地休息(线程进入等待阻塞状态 park()),但会关注前面那对情侣的动静。因为你知道,当前面的人办完,他们会来通知你(前驱节点会唤醒后继节点)。
3. 释放锁:办完离开 (Releasing the Lock)
经过一番甜蜜的填表、宣誓,窗口那对情侣(线程B)终于办完了所有手续。
第一步:腾出窗口
他们离开窗口,并且明确地通过系统把窗口状态重置为“空闲” (state 从 1 改回 0)。这就像把结婚证拿到手,然后对系统说:“我们好了,下一位!”
第二步:叫下一位
叫号系统(AQS)接收到这个信号后,不会胡乱喊人。它会严格按照排队顺序,去等候区找到排在最前面的那对情侣(队列头节点的后继节点,即下一个该被唤醒的线程),然后轻轻拍拍他们说:“轮到你们了,快去窗口吧!”(执行 unpark() 唤醒线程A)。
4. 公平 vs. 非公平的“插队”文化
这里就引出了AQS一个非常重要的特性:公平锁与非公平锁。
非公平锁 (默认情况,像现实生活)
当线程A被唤醒,走向窗口时,突然冲进来一对火急火燎的新情侣(线程C)!他们根本不看排队情况,直接冲到窗口前尝试CAS操作。
如果线程C成功了:它就“插队”成功了,线程A只能眼睁睁地看着,然后再次回到等候区排队。这虽然不公平,但效率高,因为它减少了线程切换的开销(线程C本来就在运行态,不用唤醒)。
公平锁 (理想的文明社会)
在这种模式下,系统有严格规定:必须按照排队顺序来。任何新来的情侣(线程C),即使看到窗口空闲,也必须先乖乖地去队尾取号排队。这样就保证了绝对的先来后到,杜绝了任何插队行为。
总结比喻
AQS 组件/概念 男女关系比喻
锁 / 共享资源 民政局的结婚登记窗口
线程 一对对来办理结婚登记的情侣
state 变量 窗口的“空闲/忙碌”状态指示牌
CLH 队列 民政局的等候区座位和排队顺序
CAS 操作 情侣快速查看并尝试抢占空闲窗口的敏捷动作
获取锁成功 成功占用窗口,开始办理结婚手续
获取锁失败 需要取号,进入等候区休息等待
释放锁 办完手续,离开窗口,并通知下一位
公平锁 严格按排队顺序叫号,禁止插队
非公平锁 允许新来的情侣在窗口空闲时尝试“插队”
所以,AQS的这套“排队领证”机制,完美地解决了多线程环境下,如何让众多请求者(情侣)有序、高效地访问稀缺资源(结婚窗口)的问题,既保证了秩序(公平性),又兼顾了效率(非公平性)。