乐观锁,悲观锁
通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。
乐观锁
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V。
- 进行比较的值 A。
- 要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
乐观锁是如何保证原子性的
这里的“通过原子的方式进行更新”,指的是在 CPU 硬件层面,确保“比较 V 和 A”以及“把 V 修改为 B”这两个步骤,是不可分割、不可中断的一个整体。
通俗地说:要么这两个动作一口气全做完,要么一点都不做,绝对不会出现做了一半被别人打断的情况。
1. 为什么需要“原子方式”?
假设没有原子性,分两步走:
第一步(比较):CPU 看了看内存 V 的值,发现它等于 A(比如是 0)。CPU 心想:“好,我要去把 0 改成 1。”
(中间发生切换!):就在这一瞬间,线程 B 冲进来,把内存 V 的值从 0 改成了 99。
第二步(更新):CPU 回过神来,继续执行刚才的决定,强行把 V 改成了 1。
后果:线程 B 的修改(99)被无情地覆盖了,而且线程 A 根本不知道中间发生过变化。这就是经典的并发数据覆盖问题。
2. 硬件层面的“原子”是如何实现的?
在 Java 层面调用的 CAS(比如 Unsafe.compareAndSwapInt),最终会编译成一条具体的 CPU 汇编指令。
在 x86 架构(我们常用的 Intel/AMD CPU)上,这条指令通常是:
cmpxchg (Compare and Exchange)
但光有指令还不够,为了保证在多核 CPU 同时操作时不出乱子,这条指令前通常会加一个前缀:
lock
lock cmpxchg 指令的作用:
当 CPU 执行这条指令时,它会发信号给内存总线(Bus Lock)或者利用缓存一致性协议(MESI),对目标内存地址进行“封锁”。
- 霸道声明:“在我这一条指令执行完之前(包含读取、比较、写入全过程),其他所有的 CPU 核心都不允许修改这块内存!”
3. 总结
“原子方式更新”的意思就是:
这一套动作(看一眼是不是A,如果是就改成B),在 CPU 硬件的强力保护下,对于外界来说是瞬间完成的。
外界视角:要么看到 V 还是旧值,要么看到 V 已经是新值 B。
绝对不可能出现“正在比较还没改”这种中间状态被别人趁虚而入的情况。