锁的分类

从宏观策略上划分:悲观锁 vs. 乐观锁

这是从对数据冲突的态度上划分的两种思想,而不是具体的锁。

1. 悲观锁 (Pessimistic Locking)

核心思想: 非常“悲观”,它认为数据冲突总是会发生。因此,在每次操作数据之前,都会先加锁,确保只有自己能操作,操作完成后再解锁。别人想操作?必须等我用完。

实现方式: 我们常说的互斥锁(Mutex)、信号量(Semaphore) 等传统意义上的锁,都属于悲观锁。

优点: 实现简单,能严格保证数据的一致性。

缺点: 如果冲突并不频繁,加锁和解锁的开销会很大,导致系统性能下降。因为不管有没有冲突,它都先加锁再说。

应用场景: 写多读少的场景。当多个线程频繁地修改同一个数据时,冲突的概率很高,使用悲观锁是明智的选择。例如,前面提到的抢票系统、银行账户扣款等。

2. 乐观锁 (Optimistic Locking)

核心思想: 非常“乐观”,它认为数据冲突很少发生。因此,它在操作数据时不加锁,而是在提交更新的时候去检查,看在我操作期间,这个数据有没有被别人修改过。

实现方式: 通常不依赖操作系统提供的锁机制,而是通过软件实现,最常见的两种方式是:

版本号机制 (Versioning): 在数据中增加一个 version 字段。读取数据时,连同 version 一起读出。更新时,比较当前数据库中的 version 是否与自己当初读到的一致。如果一致,就更新数据并把 version 加一;如果不一致,说明数据已被修改,本次更新失败(可以重试或报错)。

CAS (Compare-And-Swap) 算法: 这是一个原子操作,包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置V的值与预期原值A相同时,处理器才会将该位置的值更新为新值B。否则,不做任何操作。很多无锁编程(Lock-Free)技术底层都依赖于它。

优点: 在冲突少的情况下,免去了加锁解锁的开销,系统吞吐量更高。

缺点: 实现相对复杂。如果冲突频繁,会导致大量的失败和重试,性能反而会比悲观锁更差。

二、从具体实现和特性上划分

3. 互斥锁 (Mutex - Mutual Exclusion)

核心特点: 这是最基础的锁。在任何时刻,只允许一个线程持有。当一个线程获取了互斥锁后,其他任何尝试获取该锁的线程都会被阻塞(睡眠),直到锁被释放。线程被阻塞时,CPU会切换去执行其他任务,不会空转。

使用场景: 保护临界区,用于绝大多数需要保证互斥访问的通用场景。

互斥锁的设计目标非常纯粹:在任何时刻,只允许一个线程持有锁。它的上锁过程 lock() 是一个精心设计的两阶段过程。
阶段一:快速路径 (Fast Path) - 乐观尝试

当一个线程调用 lock() 时,它首先会乐观地认为锁是空闲的。
它会直接使用一条CPU原子指令(通常是 Compare-and-Swap, CAS)去尝试修改锁的状态。

阶段二:慢速路径 (Slow Path) - 请求内核帮助
如果快速路径失败了(即CAS操作发现锁的值已经是1了),说明锁正被其他线程持有。这时,线程不会傻傻地空转(像自旋锁那样)。

陷入内核:线程会执行一个系统调用(System Call),从用户态切换到内核态,请求操作系统的帮助。

进入等待队列,等待CPU调度。

4. 自旋锁 (Spinlock)

核心特点: 和互斥锁类似,也是一次只允许一个线程持有。但区别在于,当一个线程尝试获取自旋锁失败时,它不会被阻塞,而是会进入一个“忙等待”(busy-waiting)的循环,不断地检查锁是否被释放。这个过程就像线程在原地“打转”(Spin),所以叫自旋锁。

优点: 避免了线程从用户态到内核态的切换(线程阻塞和唤醒是昂贵的上下文切换)。如果锁被占用的时间非常非常短,那么自旋等待的开销会比线程切换的开销小。

缺点: 如果锁被占用的时间较长,自旋锁会持续占用CPU时间片,造成巨大的浪费。在单核CPU上使用自旋锁是毫无意义的,因为那个持有锁的线程没法运行,锁永远不会被释放。

使用场景: 内核编程中,用于对那些持有时间极短(通常是几个指令周期)的资源进行保护。在应用层编程中较少直接使用。

5. 读写锁 (Read-Write Lock)

核心特点: 它将对资源的访问者分为“读者”和“写者”。锁的规则是:

读-读共享: 当没有写者时,多个读者可以同时获取读锁,并发地读取数据。

写-写互斥: 只能有一个写者获取写锁。

读-写互斥: 当有写者持有写锁时,其他所有读者和写者都必须等待。当有读者持有时,写者必须等待。

优点: 相比于用一个互斥锁“一刀切”地锁住所有操作,读写锁大大提高了系统的并发能力,尤其是在“读多写少”的场景下。

缺点: 实现比互斥锁复杂,且可能导致“写者饥饿”(当读者源源不断时,写者可能永远拿不到锁)。

使用场景: 适用于读多写少的并发场景,例如配置文件读取、缓存系统等。

6. 递归锁 (Recursive Lock) / 可重入锁 (Reentrant Lock)

核心特点: 允许同一个线程多次获取同一个锁而不会产生死锁。锁内部会维护一个计数器,线程每获取一次锁,计数器加一;每释放一次锁,计数器减一。当计数器归零时,锁才被真正释放,其他线程才能获取。

解决的问题: 在一个函数中获取了锁,然后又调用了另一个也需要获取同一个锁的函数。如果使用普通的互斥锁,线程会因为尝试获取自己已经持有的锁而陷入死锁。

使用场景: 当代码中存在递归调用或者函数调用链,且这些函数都需要访问同一个被锁保护的资源时。例如,Java中的 ReentrantLock 和 synchronized 关键字都是可重入的。

posted @ 2025-08-05 09:23  浪矢-CL  阅读(39)  评论(0)    收藏  举报