悲观锁、乐观锁、自旋锁、互斥锁怎么区分
这一组概念非常容易混,因为它们看起来都和“锁”有关,但其实不在同一层面。
理解这组概念最重要的一句话是:
悲观锁和乐观锁,是处理并发冲突的思路;自旋锁和互斥锁,是具体的加锁实现方式。
如果不先把这两层分开,就很容易越学越乱。
一、先把四个概念分层
1. 悲观锁、乐观锁
这是:
并发控制策略
也就是说,它们描述的是:
- 你如何看待冲突
- 冲突发生前要不要先防住
- 冲突发生后如何处理
2. 自旋锁、互斥锁
这是:
具体的加锁方式 / 等待方式
也就是说,当你已经决定“要加锁”之后,拿不到锁的时候到底怎么办。
二、什么是悲观锁
悲观锁的核心思想是:
先假设并发冲突很容易发生,所以先把资源锁住,再操作。
也就是说,我在改数据之前先上锁,其他线程先别碰,等我做完了再放开。
典型例子
synchronizedReentrantLock- 数据库里的
select ... for update - 互斥锁
- 自旋锁
所以从归类上说:
自旋锁和互斥锁通常都属于悲观锁思路下的实现。
优点
- 冲突多时更稳
- 一旦拿到锁,后续逻辑比较确定
- 比较符合直觉
缺点
- 线程可能大量阻塞等待
- 上下文切换开销变大
- 加锁不当可能带来死锁
三、什么是乐观锁
乐观锁的核心思想是:
先假设并发冲突不常发生,所以先不加传统锁,等提交时再检查数据有没有被别人改过。
如果检查时发现:
- 没人改过 → 更新成功
- 已经被改过 → 更新失败 / 重试
常见实现方式
1. 版本号
例如数据库表里维护一个 version 字段:
- 读数据时拿到
version = 5 - 更新时带条件
where version = 5 - 成功后把版本加 1
- 如果版本不对,说明别人已经先改了
2. CAS
CAS(Compare And Swap)是乐观锁最经典的一种底层实现思路。
它会做:
- 比较当前值是不是预期值
- 如果是,就更新
- 如果不是,就失败 / 重试
优点
- 不容易阻塞
- 并发度通常更高
- 更适合读多写少、冲突少的场景
缺点
- 冲突多时会频繁失败和重试
- 实现通常比悲观锁更绕
- 重试会带来额外开销
四、什么是自旋锁
自旋锁的核心特点是:
拿不到锁时,不睡眠,而是一直循环尝试获取锁。
最经典的基础模型可以理解成:
- 锁变量为
0表示空闲 - 锁变量为
1表示已占用 - 线程反复原子地尝试把
0改成1
这通常依赖:
- CAS
- Test-and-Set
- Exchange
等原子操作。
为什么叫“自旋”
因为线程拿不到锁时,不挂起、不睡眠,而是一直 while 循环地尝试,就像原地打转一样。
优点
- 如果临界区很短,锁很快释放,自旋往往比睡眠 / 唤醒更省事
- 适合多核场景、短临界区
缺点
- 如果锁持有时间长,会白白浪费 CPU
- 单核场景下通常更不友好
五、什么是互斥锁
互斥锁的核心特点是:
拿不到锁时,线程会睡眠 / 阻塞,等待别人释放锁后再被唤醒。
也就是说,和自旋锁不同,互斥锁不会一直空转,而是:
- 拿不到锁
- 先挂到等待队列
- 让出 CPU
- 等锁可用时再被唤醒
优点
- 不白白浪费 CPU
- 更适合临界区较长、等待时间可能较久的场景
缺点
- 睡眠 / 唤醒 / 上下文切换有额外开销
- 相比自旋锁,拿锁过程更重一点
六、自旋锁和互斥锁最本质的区别
你可以直接记:
自旋锁是“拿不到就一直转”,互斥锁是“拿不到就先去睡”。
更具体地说:
- 自旋锁:忙等待
- 互斥锁:阻塞等待
所以它们都属于“锁”,但等待策略不同。
七、四者放在一起怎么统一理解
1. 悲观锁 vs 乐观锁
这是“思想层”的区别:
- 悲观锁:先防住冲突
- 乐观锁:先不防,提交时再检测
2. 自旋锁 vs 互斥锁
这是“实现层”的区别:
- 自旋锁:忙等待拿锁
- 互斥锁:阻塞等待拿锁
3. 它们之间的关系
- 自旋锁通常属于悲观锁实现
- 互斥锁通常也属于悲观锁实现
- CAS / 版本号通常属于乐观锁实现
八、为什么 CAS 看起来也像“自旋”但不等于自旋锁
这个很容易混。
很多 CAS 写法会像这样:
while (!CAS(...)) {
// 重试
}看起来也在循环,也像“自旋”。
但要注意:
CAS 的“循环重试”
是:
乐观锁失败后的重试策略
自旋锁
是:
为了拿到悲观锁,在原地忙等锁释放
所以虽然它们表面都可能在 while 循环,但本质不一样。
九、什么场景更适合谁
悲观锁适合
- 写冲突频繁
- 临界区逻辑复杂
- 不想频繁失败重试
- 强一致要求高
乐观锁适合
- 读多写少
- 冲突不频繁
- 能接受失败重试
- 追求更高并发度
自旋锁适合
- 临界区很短
- 多核系统
- 锁预计很快就会释放
互斥锁适合
- 临界区较长
- 线程可能等待较久
- 更通用、更稳妥的场景
十、条件变量、读写锁、公平锁和非公平锁顺手补充一下
前面把四个最容易混的概念先分清了,但在并发控制里,还经常会继续追问这几个:
- 条件变量
- 读写锁
- 公平锁 / 非公平锁
它们同样很容易和前面的几种锁混在一起,所以顺手一起补清楚。
1. 条件变量
条件变量最核心的作用不是“加锁”,而是:
让线程在某个条件不满足时先等待,等条件满足后再被唤醒。
你可以把它理解成:
- 锁解决的是“同一时刻能不能一起进”
- 条件变量解决的是“现在该不该继续做”
为什么条件变量通常要和互斥锁一起用
因为条件变量等待的是“共享状态”。
例如:
- 队列为空,消费者要等
- 队列不满,生产者才能继续放
而这个“队列是否为空 / 满”的状态本身,需要互斥锁保护。否则会出现:
- 线程 A 检查条件后准备睡眠
- 线程 B 已经修改状态并发出通知
- 线程 A 错过通知
所以条件变量通常需要和互斥锁配合使用:
- 互斥锁保护共享状态
- 条件变量负责等待与唤醒
一个最重要的写法习惯
条件变量常常要配合 while,而不是简单 if。
因为线程被唤醒后,不代表条件一定仍然满足,还需要重新检查一次。
2. 读写锁
读写锁适合解决这样的问题:
读操作很多,写操作较少,而且读和写的并发规则不一样。
它最核心的规则是:
- 读-读可以并发
- 读-写互斥
- 写-写互斥
所以读写锁特别适合:
- 配置读取
- 缓存查询
- 路由表读取
- 读多写少的数据结构
为什么它能提高吞吐量
如果用普通互斥锁,那么:
- 读也互斥
- 写也互斥
读线程之间本来互不影响,却也被串行化了。
而读写锁可以让多个读线程一起进,从而提高并发度。
它的代价是什么
- 实现更复杂
- 维护成本更高
- 如果策略设计不当,可能会导致写者饥饿
3. 公平锁
公平锁的核心思想是:
谁先等待,谁先拿锁。
也就是严格按排队顺序分配锁。
优点
- 更公平
- 不容易让某些线程长期拿不到锁
缺点
- 队列维护更严格
- 吞吐量通常会低一点
4. 非公平锁
非公平锁的核心思想是:
不严格按等待顺序来,谁有机会抢到谁先拿。
也就是说,后来的线程也可能直接插队抢到锁。
优点
- 吞吐量通常更高
- 减少严格排队和唤醒切换的开销
缺点
- 公平性更差
- 某些线程可能长期抢不到锁,出现饥饿
5. 把它们和前面的几种锁统一起来
你可以这样理解:
- 悲观锁 / 乐观锁:并发控制策略
- 自旋锁 / 互斥锁:具体加锁与等待方式
- 条件变量:等待条件成立的机制
- 读写锁:区分读和写访问规则的锁
- 公平锁 / 非公平锁:锁的分配策略
所以它们虽然都和并发控制有关,但并不在同一层面。
十一、最容易混的地方
1. 乐观锁不是“完全不冲突”
不是。它只是:
不提前阻止,而是在提交时检查冲突。
2. 自旋锁不是“更高级的锁”
不是。它只是等待方式不同:
- 拿不到就一直转
3. 互斥锁不等于“程序里只能一个线程运行”
不是。它只是:
同一时刻只能一个线程进入某个被保护的临界区
4. 乐观锁不等于完全不用同步原语
不是。它通常仍然依赖:
- 原子操作
- CAS
- 内存语义保证
只是不用传统的“先加锁再进”。
十一、最后给一个总对比
你可以这样记:
悲观锁
先锁住,别抢
乐观锁
先干,出冲突再说
自旋锁
拿不到就在门口一直转
互斥锁
拿不到就先去睡,等叫号
十二、总结
悲观锁和乐观锁是并发控制策略,前者假设冲突很容易发生,因此先加锁再操作;后者假设冲突不常发生,因此通常不先加传统锁,而是在提交时通过 CAS 或版本号检测是否发生冲突。自旋锁和互斥锁则是更具体的锁实现方式,它们通常都属于悲观锁思路,区别在于拿不到锁时是忙等待还是阻塞睡眠。把这四个概念分成“策略层”和“实现层”去理解,就不会轻易混淆。
