1.AQS
核心问题: 当多个线程(想象成办事的人)想抢着用同一个资源(比如一个公共厕所隔间、一个银行柜台、或者一个共享计数器)时,怎么让它们不打架、不混乱、有秩序地轮流使用?
AQS(AbstractQueuedSynchronizer,抽象队列同步器) 就是 Java 大神们写出来专门解决这个排队问题的核心工具箱。很多你熟悉的并发工具(像 ReentrantLock, Semaphore, CountDownLatch, ReentrantReadWriteLock 等等)的底层排队机制,都是靠 AQS 来实现的。
AQS 的核心思想和工作原理,可以类比成一个银行叫号系统:
核心状态(State): 就像银行里当前开放的柜台数量。
这个 state 是一个整数变量,是 AQS 的心脏。对于锁来说,state=0 表示锁空闲,state=1 表示锁被一个线程占用(如果是可重入锁,state 可以 >1,表示同一个线程重入了多少次)。对于信号量(Semaphore)来说,state 就表示当前可用的许可证数量。关键操作: AQS 提供 getState(), setState(), compareAndSetState()(CAS 操作) 这些方法来安全地读取和修改这个状态值。CAS 操作是保证并发安全的基石。 等待队列(CLH 队列): 就像银行里的等候区。
当所有柜台都忙(对于锁就是锁已被占用,对于信号量就是许可证用光了)时,新来的线程(顾客)不能立刻办事,就需要去排队。AQS 内部维护了一个先进先出(FIFO)的虚拟队列(通常基于链表实现)。每个排队的线程被封装成一个节点(Node)放到队列尾部。这个队列就是保证公平性的关键(公平锁会严格按这个队列顺序叫号,非公平锁则允许新来的线程尝试插队抢一下)。 获取资源(Acquire): 顾客尝试办事。
当一个线程(顾客)想要获取资源(比如获取锁、获取许可证)时,它首先会看一眼状态(柜台数量/锁是否空闲)。如果资源可用(state 满足条件,比如 state > 0):它会尝试用 CAS 操作快速“占坑”(比如把 state 减 1)。如果成功了,它就成功获取资源,可以去“办事”了(执行临界区代码)。如果失败了(可能被别的线程抢先了),它也得去排队。如果资源不可用(state 不满足条件,比如 state == 0)或者抢失败了:
它会被封装成一个 Node 节点。它会被安全地加入到等待队列的尾部。加入队列后,它会让自己进入“睡眠”状态(调用 LockSupport.park()),等着被叫号(唤醒)。这时候线程就被阻塞了,不会消耗 CPU。 释放资源(Release): 顾客办完事了。
当一个线程(顾客)用完了资源(比如释放锁、归还许可证),它会调用 release 方法。这个方法会做两件重要的事:
更新状态(State):比如把 state 加 1(表示释放了一个资源/许可证)。“叫号”:它会去检查等待队列的头部节点(排在最前面的那个线程/顾客)。
如果头结点存在,它会尝试唤醒(unpark) 头结点代表的那个线程。被唤醒的线程(顾客)会从之前被阻塞的地方醒来,然后再次尝试去获取资源(再次尝试 CAS 修改 state 来占坑)。如果这次成功了,它就可以离开队列去“办事”了。如果失败了(可能又被新来的线程或其它原因抢走了),它可能又得重新进入睡眠状态排队(具体行为取决于实现)。 可重入性(Reentrancy): 同一个顾客可以连续办多件事。
想象一个 VIP 顾客(线程)已经占了一个柜台(获取了锁),他还没办完事,又想在这个柜台再办另一件事(再次获取同一把锁)。AQS 支持这个特性。它通过 state 值来记录同一个线程获取锁的次数。第一次获取时 state=1,第二次获取时 state=2,每次释放就减 1,直到 state=0 才算完全释放锁,其他线程才有机会获取。排队队列里的节点会记录它属于哪个线程,这样当同一个线程再次尝试获取时,AQS 知道是自己人,允许它直接增加 state 值而不需要重新排队。 独占 vs 共享:
独占模式(Exclusive): 资源一次只能被一个线程使用。典型例子就是 ReentrantLock。银行类比就是一个独享的 VIP 包间,一次只能进一个顾客(线程)。共享模式(Shared): 资源可以同时被多个线程使用(数量由 state 控制)。典型例子是 Semaphore 和 CountDownLatch。银行类比就是多个公共柜台,只要有空闲柜台 (state > 0),多个顾客(线程)可以同时办理业务。
总结一下 AQS 的精髓:
一个状态(State): 核心计数器,表示资源是否可用/有多少可用。一个队列: 管理那些暂时抢不到资源的线程,让它们有序排队等待。CAS + Park/Unpark:
CAS: 用来快速、安全地修改 state,实现无锁化的资源抢占尝试。Park: 让抢不到资源的线程去队列里“睡觉”(阻塞),不浪费 CPU。Unpark: 当资源被释放时,叫醒队列头部的线程(或者某些共享模式下可能叫醒多个线程)来尝试获取资源。 模板方法模式: AQS 本身是抽象的。它定义了排队、阻塞、唤醒的骨架逻辑。具体的资源获取和释放条件(比如“什么时候才算获取成功?”、“释放时应该把 state 改成多少?”)由子类(如 ReentrantLock, Semaphore 的内部类)来实现。这就是它名字里“抽象”的含义。
为什么说 AQS 重要?
基石: 它是 Java 并发包里很多强大工具(锁、信号量、栅栏等)的核心发动机。理解 AQS,你就理解了这些工具底层是怎么高效、安全地管理线程协作的。高效: 它巧妙地结合了 CAS(无锁,快速路径) 和 FIFO 队列 + Park/Unpark(阻塞,慢速路径),在大多数资源不冲突的情况下性能很高,只在真正需要排队时才阻塞线程。灵活: 通过实现几个关键的保护方法(tryAcquire, tryRelease, tryAcquireShared, tryReleaseShared),开发者可以基于 AQS 构建出各种复杂的同步组件。
简单来说:AQS 就是一个管理线程排队、叫号、睡觉、起床的超级管理员。它用一个数字(state)表示资源情况,用一个队列让抢不到的线程乖乖排队睡觉,等资源空出来了,就按顺序(或规则)叫醒队头的线程起来干活。 理解了银行叫号系统,就基本理解了 AQS 的核心思想。
2.CAS
⚡ CAS 的本质:先看再改
假设你和朋友合租,冰箱里只剩最后一瓶可乐🍹:
1. **你看到**:冰箱里有可乐(当前值 = 可乐)
2. **你计划**:如果还是可乐,就换成雪碧(期望值 = 可乐,新值 = 雪碧)
3. **动手时**:快速开冰箱门看一眼:
- ✅ 还是可乐 → 立刻换成雪碧(成功!)
- ❌ 变成凉茶 → 放弃(失败!)
💡 关键点:整个过程是原子操作(开冰箱门到换饮料一气呵成,不会被室友打断)
🛠️ 技术对应关系
生活场景CAS 操作计算机概念冰箱里的饮料内存中的值共享变量(如 int count)“看一眼”的动作Compare(比较)检查内存值是否等于预期值换饮料Swap(交换)更新为新值一气呵成的操作CPU 原子指令硬件级保证不可打断
🔁 工作流程(Java代码版)
// 伪代码:模拟AtomicInteger的CAS操作
public class MyCounter {
private volatile int value; // volatile保证可见性
// CAS核心操作
public boolean compareAndSwap(int expected, int newValue) {
if (value == expected) { // 检查:当前值==期望值吗?
value = newValue; // 交换:条件成立则更新
return true;
}
return false;
}
// 实际使用:线程安全的累加
public void increment() {
int oldVal;
do {
oldVal = value; // 读取当前值
int newVal = oldVal + 1; // 计算新值
} while (!compareAndSwap(oldVal, newVal)); // 失败就重试!
}
}
🌰 实际生活案例
1. 抢演唱会门票(无锁竞争)
你查系统:余票=1张
立刻点击购买 → 系统用CAS操作:
if (余票 == 1) { 余票=0; 出票成功; } // CAS成功!
2. 多人抢票(有竞争)
你查系统:余票=1同时小王也查到余票=1你的CAS:余票==1? → 设为0(成功)小王的CAS:余票==1? → 此时余票已是0 → 失败!重试 → 小王刷新页面看到“已售罄”
⚠️ 经典问题:ABA 事件
想象你追女神👩的场景:
1. 你看到女神 **单身(状态A)**
2. 你去准备表白礼物(耗时)
3. 回来用CAS:`if (女神==单身) { 表白! }`
问题:中间有个前任悄悄复合又分手了(A→B→A)! 结果:你以为女神一直单身,其实已被绿过🌿
解决方案:加版本号!
AtomicStampedReference<感情状态> ref =
new AtomicStampedReference<>("单身", 1 /*版本号*/);
// 表白前记录版本号
int stamp = ref.getStamp();
// 表白时检查:状态是单身 && 版本号未变
ref.compareAndSet("单身", "恋爱中", stamp, stamp+1);
✅ CAS 的适用场景
场景例子优势简单计数网站访问量统计比锁快10倍以上状态标记订单是否已支付无锁避免线程阻塞无锁数据结构ConcurrentHashMap的桶操作细粒度并发控制
🆚 CAS vs 锁(synchronized)
对比项CAS锁工作方式乐观重试(相信冲突少)悲观阻塞(默认会冲突)性能低竞争时快如闪电⚡总要排队慢半拍🐢CPU消耗高竞争时疯狂重试(CPU空转)线程休眠(省CPU但上下文切换慢)复杂度需处理ABA、自旋策略简单粗暴适用场景简单变量更新复杂逻辑块(如转账:查余额+计算+更新)
💡 一句话总结
CAS = 乐观的快速试探
像闪电侠一样尝试修改内存值: 看一眼 → 相等就改 → 不等就撤适合轻量级操作(计数/标记)高竞争时可能忙到CPU冒烟💨
3.ConcurrentHashMap
🧳 1. 普通储物柜(HashMap)的问题
单门储物柜(Hashtable):所有人存取东西必须排队,一个人用的时候其他人干等着。没锁的储物柜(HashMap):大家随便抢着用,最后谁放了什么、东西丢没丢全乱套。
🚀 2. ConcurrentHashMap 的解决方案
它把大储物柜拆成了很多小格子(桶),每个格子带独立智能锁:
🔑 核心黑科技:
智能锁机制:
你存取东西时,只锁你用的那个格子(其他格子别人照样用)锁超快:发现格子没人用时秒开(CAS),有人用时才上小锁(synchronized) 动态扩容:
当格子快满时,系统悄悄复制双倍格子搬运工(线程)边用边搬:你取东西时顺便帮搬几件(多线程协作) 高效找东西:
小件物品挂钩子上(链表) → 东西多了升级成货架(红黑树)找东西不用等:随时看当前最新挂着的物品(无锁读)
🛠️ 3. 怎么存东西(put操作)
假设你要存行李箱(key=张三,value=行李箱):
算格子号:张三的名字 → 哈希计算 → 3号格子看格子状态:
✅ 空格子 → 直接挂上去(CAS秒操作)❌ 有人正在用 → 等他用完小锁解开(等0.1毫秒)🚧 正在扩容 → 顺手帮他搬几件物品(协助扩容) 挂物品:
挂钩子(链表):如果挂钩没满上货架(红黑树):如果挂钩超过8个物品
💡 全程只锁3号格子!1号2号格子别人照样存取
🔍 4. 怎么取东西(get操作)
更简单!直接看格子:
找到对应格子(比如3号)不用等锁:瞄一眼挂钩/货架上的物品找到“张三”标签 → 拿走行李箱
⚡ 全程无等待,哪怕别人正在隔壁格子存东西
📈 5. 为什么比Hashtable快?
场景HashtableConcurrentHashMap10个人存不同格子排队1小时同时存,1分钟搞定取东西时有人存东西干等照取不误储物柜满了要扩容停业整顿边用边搬
🧩 6. 特别注意
不是万能:复合操作需注意(比如“没有才存”要用 putIfAbsent())弱一致性:你取东西时看到的是当前瞬间的储物柜状态(可能刚搬完货)统计数量:格子太多时,计数是估算值(避免全局锁)
💎 总结一句话
ConcurrentHashMap = 分组小锁 + 无锁快读 + 协同搬家 + 动态升级 让多线程像逛超市一样: 🔹 不同货架随便逛(并行读) 🔹 买商品不堵收银台(细粒度锁) 🔹 理货员默默补货不挡路(后台扩容)
4.volatile
在 Java 中,volatile 关键字用于保证变量的可见性,即当一个变量被声明为 volatile 时:
对该变量的写操作会立即刷新到主内存;对该变量的读操作会从主内存中读取最新值。
🧙♂️ 超能力1:禁止指令重排(有序性)
想象厨师做菜🍳的指令:
1. 开火
2. 倒油
3. 打鸡蛋
问题:JVM(像任性的厨子)可能乱序执行 → 先打鸡蛋再开火💥 volatile 解决方案: 👉 在关键步骤插旗帜🚩:“必须按顺序来!”
👁️ 超能力2:强制刷内存(可见性)
多线程操作变量就像办公室白板:
- 主内存:中央白板(唯一真相来源)
- 工作内存:员工各自的小本本📒
问题: 线程A修改白板:“预算=100万” → 但线程B的小本本还记着"预算=50万" volatile 解决方案: 👉 只要写 volatile 变量,强制所有人小本本作废 → 必须看中央白板!
⚠️ 重要限制:不保证原子性!
volatile int count = 0; 错误认知:count++ 是安全的 真相:
// 分解动作:
1. 读取count值 (如读到100)
2. +1计算 (得到101)
3. 写回count
👉 若两个线程同时读到100 → 都算出101 → 结果应是102却变成101!
💡 使用场景(适合三种情况)
场景示例原理状态标志volatile boolean isRunning = true;关停时立刻对所有线程可见单次安全发布volatile User user = new User();防止构造函数重排独立观察变量volatile long lastUpdateTime;无需原子性,只需可见
❌ 不适合的场景
// 场景:多线程计数器
private volatile int count = 0;
public void add() {
count++; // 危险!非原子操作
}
// 正确方案:用AtomicInteger或锁
private final AtomicInteger safeCount = new AtomicInteger(0);
🔧 底层原理(内存屏障)
屏障类型作用写屏障确保volatile写之前的操作不会重排到写之后读屏障确保volatile读之后的操作不会重排到读之前
// x86硬件指令(实际是lock指令前缀)
lock addl $0, 0(%rsp) // 空操作+内存屏障效果
🆚 volatile vs synchronized
特性volatilesynchronized可见性✅ 保证✅ 保证有序性✅ 部分保证(禁止重排)✅ 完全保证(管程内串行)原子性❌ 不保证✅ 保证阻塞❌ 不阻塞线程✅ 可能阻塞粒度变量级别代码块级别
🌰 真实案例:单例模式(双重检查锁)
public class Singleton {
private static volatile Singleton instance; // 必须volatile!
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Singleton(); // ⚠️ 无volatile可能看到半初始化对象!
}
}
}
return instance;
}
}
关键作用:防止 new Singleton() 时指令重排导致其他线程拿到未初始化的对象
💎 总结一句话
volatile = 变量穿上了防弹衣🧊
防弹衣1:顺序守护(禁止重排序)防弹衣2:光速同步(写立即全局可见) 但防弹衣不防匕首🔪(不保护复合操作)