锁
概念解析
悲观锁 / 乐观锁
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 核心思想 | 认为并发冲突很可能发生,先加锁再操作数据。 | 认为冲突较少发生,直接操作数据,提交时检查冲突。 |
| 实现方式 | synchronized、ReentrantLock、数据库行锁。 | CAS、版本号机制(如数据库 version 字段)。 |
| 适用场景 | 写操作频繁、竞争激烈。 | 读多写少、低竞争环境。 |
| 性能开销 | 锁管理(获取/释放)开销大。 | 无锁,但冲突重试可能增加开销。 |
1. CAS(Compare-And-Swap)
-
定义:
CAS 是一种无锁(Lock-Free)的原子操作,用于在多线程环境中实现同步。它的操作逻辑是:
只有当变量的当前值与预期值一致时,才将其更新为新值。整个过程是原子性的,确保线程安全。是 乐观锁。 -
伪代码表示:
boolean CAS(V, expected, newValue) { if (V.value == expected) { V.value = newValue; return true; } return false; } -
应用场景:
- 实现无锁数据结构(如无锁队列、栈)。
- Java 中的
AtomicInteger、AtomicReference等原子类。 - 数据库乐观锁(通过版本号机制)。
-
优缺点:
- ✅ 避免锁开销,适用于低竞争场景。
- ❌ 高竞争时可能导致大量自旋(CPU 空转)。
- ❌ ABA 问题(可通过版本号或标记解决)。
2. 自旋锁(Spin Lock)
-
定义:
线程在尝试获取锁时,若锁已被占用,会通过循环(自旋)不断检查锁状态,而非立即阻塞。
属于悲观锁的一种实现(默认会有竞争,需主动获取锁)。 -
适用场景:
锁持有时间极短(如内核短临界区),避免线程切换的开销。 -
伪代码示例:
while (!tryAcquireLock()) { // 自旋等待,占用 CPU } -
优缺点:
- ✅ 无上下文切换,响应快。
- ❌ 占用 CPU 资源,不适合长时间阻塞。
3. 互斥锁(Mutex Lock)
-
定义:
线程获取锁失败时,会进入阻塞状态(让出 CPU),等待锁释放后被唤醒。
也属于悲观锁,但通过线程休眠避免 CPU 浪费。 -
适用场景:
锁持有时间较长或竞争激烈的情况。 -
伪代码示例:
acquireLock(); try { // 操作共享资源 } finally { releaseLock(); } -
优缺点:
- ✅ 不占用 CPU 资源。
- ❌ 线程切换带来额外开销。
4. 读写锁(Read-write Lock)
总结
- CAS:乐观锁的基石,通过原子性比较-交换避免锁开销,需处理 ABA 问题。
- 自旋锁:悲观锁的一种,通过忙等待减少切换开销,适合短临界区。
- 互斥锁:悲观锁的典型,通过阻塞线程节省 CPU,适合长临界区。
- 选择策略:
- 低竞争、快速操作 → 乐观锁(CAS)。
- 高竞争、短临界区 → 自旋锁。
- 高竞争、长临界区 → 互斥锁。
原子化
cmpxchg: Compare and Exchange
通过 EAX 寄存器存储预期值,并与内存或寄存器中的目标值比较,若相等则交换为新值。
原子化操作 = 加锁
条件变量
- 通常,临界区代码只有在特定条件满足时才需要执行。
- 这时可以使用条件变量:
- 线程先获取临界区的锁,然后调用条件变量等待条件成立。
- 等待线程会原子性地释放锁并进入休眠状态。
- 若有多个等待线程,它们将被放入队列。
- 当收到
signal_all信号时,其中一个被唤醒的线程会重新获取锁。
- 这种方式比不断轮询锁状态以检查条件是否满足更高效。
wait(cond, lock)- 原子性地释放锁并休眠。唤醒时尝试重新获取锁。
signal(cond)- 唤醒一个在
cond上等待的休眠线程。
- 唤醒一个在
signal_all(cond)- 唤醒所有在
cond上等待的休眠线程。
- 唤醒所有在