系统性能优化之锁篇

系统性能优化之锁篇

多线程下为了确保程序不会出错,必须加锁后才能访问共享资源。锁也是有层级之分的,在不同的场景下加不同的锁,对系统性能也有着很大影响。

互斥锁与自旋锁

互斥锁与自旋锁是最底层的两个锁,其他的锁几乎都是基于此实现的。**互斥锁加锁失败时会释放CPU进入阻塞状态,自旋锁则会忙等待**。

**当无法确定锁住的代码需要执行多久时,应当首选互斥锁。**互斥锁是一种独占锁,当被线程取得后,除非线程释放,其他线程的取锁代码都会阻塞。阻塞通常是由操作系统内核实现的,取锁失败时将线程置于休眠状态,当锁释放后寻找合适的时机唤醒线程。

image-20250625105013432

但是线程取锁失败时的阻塞与唤醒增加了上下文切换的成本,或许这段时间比锁住的代码段执行时间还长。而且,线程主动进入休眠是高并发服务无法容忍的行为,这让其他异步请求都无法执行。

如果被锁住的代码执行时间很短,就应该用自旋锁取代互斥锁。

自旋锁比互斥锁快得多,因为它通过 CPU 提供的 CAS 函数(全称 Compare And Swap),在用户态代码中完成加锁与解锁操作。它将比较与交换合成为一条硬件级原子指令,避免了多线程下的可能出现的问题。

假设锁为变量 lock,整数 0 表示锁是空闲状态,整数 pid 表示线程 ID,那么 CAS(lock, 0, pid) 就表示自旋锁的加锁操作,CAS(lock, pid, 0) 则表示解锁操作。多线程竞争锁的时候,加锁失败的线程会进入“忙等待”,但这并不意味着一直执行 CAS 函数,生产级的自旋锁在“忙等待”时,会与 CPU 紧密配合 ,它通过 CPU 提供的 PAUSE 指令,减少循环等待时的耗电量;对于单核 CPU,忙等待并没有意义,此时它会主动把线程休眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (true) {
// 因为判断lock变量的值比CAS操作更快,所以先判断 lock 再调用 CAS 效率更高
if (lock == 0 && CAS(lock, 0, pid) == 1) return;

if (CPU_count > 1 ) { //如果是多核CPU,“忙等待”才有意义
for (n = 1; n < 2048; n <<= 1) { // pause 的时间,应当越来越长
for (i = 0; i < n; i++) pause(); // CPU 专为自旋锁设计了 pause 指令
if (lock == 0 && CAS(lock, 0, pid)) return; // pause后再尝试获取锁
}
}

// 单核 CPU,或者长时间不能获取到锁,应主动休眠,让出CPU
sched_yield();
}

当取不到锁时,互斥锁用“线程切换”来面对,自旋锁则用“忙等待”来面对。这是两种最基本的处理方式,更高级别的锁都会选择其中一种来实现,比如读写锁就既可以基于互斥锁实现,也可以基于自旋锁实现。

读写锁

如果项目中能明确的区分出读写场景,则可以选择读写锁。

读写锁的优势在于:当写锁未被持有时,读锁可以被多个线程并发持有,提高了资源的使用率。而当写锁被持有后,获取读锁和写锁的线程都会被阻塞。写锁是独占锁,读锁是共享锁。

因此,读写锁真正发挥优势的场景,必然是读多写少的场景,否则读锁将很难并发持有。

实际上,读写锁既可以倾向于读线程,又可以倾向于写线程。前者我们称为读优先锁,后者称为写优先锁。

读优先锁更强调效率,它期待锁能被更多的线程持有:当读锁被持有时,先来一个线程希望取写锁,又来一个线程希望取读锁,即使取写锁的线程先来,后续前来获取读锁的线程依然可以立刻加锁成功,因为这样两个线程在并发持有锁,效率更高。当然如果有源源不断的读线程,则会导致写线程饥饿。

image-20250625111249735

写优先锁由于写锁的优先级更高,当有源源不断的写线程,则会导致读线程饥饿。

image-20250625111309693

除此之外还有读写公平锁,按照请求顺序排队,读线程依然可以并发持有读锁,但是无法“加队”到写线程前

乐观锁

无论互斥锁、自旋锁还是读写锁,都属于悲观锁。“乐观”,就是假定冲突的概率很低,所以它采用的“加锁”方式是,先修改完共享资源,再验证这段时间内有没有发生冲突。如果没有其他线程在修改资源,那么操作完成。如果发现其他线程已经修改了这个资源,就放弃本次操作。

至于放弃后如何重试,则与业务场景相关,虽然重试的成本很高,但出现冲突的概率足够低的话,还是可以接受的。可见,乐观锁全程并没有加锁,所以它也叫无锁编程。

例如在线修改文档,可以记录下获取到当前文档时的版本号,在提交时携带原始版本号,如果期间有其他用户修改,则原始版本号无法对应,就放弃本次修改。

1
update #{table} set version = #{newVersion} where id = #{id} and version = #{oldVersion}

image-20250625110845779

乐观锁除了应用在 Web 分布式场景,在数据库等单机上也有广泛的应用。只是面向多线程时,最后的验证步骤是通过 CPU 提供的 CAS 操作完成的。

乐观锁虽然去除了锁操作,但是一旦发生冲突,重试的成本非常高。所以,只有在冲突概率非常低,且加锁成本较高时,才考虑使用乐观锁。

小结

互斥锁能够满足各类功能性要求,特别是被锁住的代码执行时间不可控时,它通过内核执行线程切换及时释放了资源,但它的性能消耗最大。如果能够确定被锁住的代码取到锁后很快就能释放,应该使用更高效的自旋锁。

如果能区分出读写场景,读写锁是第一选择,它允许多个读线程同时持有读锁,提高了并发性。

当并发访问共享资源,冲突概率非常低的时候,可以选择无锁编程。

总之,**不管使用哪种锁,锁范围内的代码都应尽量的少,执行速度要快。**


系统性能优化之锁篇
https://zhuwenjie0716.github.io/2026/04/27/系统性能优化之锁篇/
作者
Wenjie Zhu
发布于
2026年4月27日
许可协议