关于锁

tags: OS
@面 10/09/2026

竞态条件是指当多个线程同时访问和修改同一块未被保护的内存区域的时候,最终结果依赖于线程的执行顺序,可能导致数据的不一致 为了保护共享资源,确保共享资源同一时间只有一个线程可以修改,用锁来保护。锁分很多类型

自旋锁

对于一个用自旋锁保护的资源,线程抢锁失败之后,会忙等待,直到它拿到锁。 自旋锁不会主动产生上下文切换,是在用户态进行由硬件封装的原子指令,包含

  • 检查锁的状态
  • 将锁设置为当前线程持有 两个步骤。忙等待可以用while循环实现,最好用pause实现,可以减少循环等待时候的耗电量。 自旋锁的好处是没有上下文的切换,加锁解锁很快。缺点是忙等待期间一直占用CPU资源,锁很长时间会浪费CPU,而且临界区非常短 (临界区就是指被锁住的,只能一个线程访问修改的那一段代码)

互斥锁

线程如果拿不到互斥锁,那么阻塞挂起等待。等待持有锁的线程释放锁之后唤醒等待线程。在linux中由futex唤起 互斥锁的好处是不会长时间占用CPU,适合临界区比较长。但是阻塞唤醒有开销,线程上下文切换也有开销。

读写锁

允许多个线程同时读,但是只要写操作到来,立刻阻塞读线程和读操作,同时也不允许其他写操作进行 读写锁适合多读少写的场景,但是实现比较复杂,而且锁本身有管理开销,写操作很多的时候性能不如互斥锁。

乐观锁

乐观锁偏向于一种实现思想,乐观锁认为大多数时间不会发生冲突。对于一个值,先检查,再修改,然后提交操作,如果不冲突则认为成功,反之认为失败。 举一个KV的例子,在MVCC场景下,存入一个建 key:name,value:1,expectversion:1,那么操作先检查当前建版本是否为1,如果是则修改,否则建修改认为失败 乐观锁不需要一直持有锁,并发性能好,在冲突小的场景下效率很高,但是高并发高冲突场景下要不断重试。而且存在ABA问题 ABA问题就是一个线程检查CAS,为A则修改。但是在检查到A之后挂起等待。此时另一个线程修改值为B,再修改回A,原线程恢复执行,虽然值还是A,但是实际上已经发生修改,这样的问题可以用检查值+全局版本号的方法解决

悲观锁

悲观锁和乐观锁一样属于一种实现思想,但是完全相反。悲观锁认为冲突一定会发生,所以要求每次操作都先锁住临界区,然后操作,最后释放锁。

SELECT * FROM product
WHERE id = 1
FOR UPDATE;

上面的例子是一个锁定读,加一个排它锁(和互斥锁不完全一样)

产生死锁的四个条件

  1. 互斥条件
  2. 持有并等待条件(A持有1条件等待2,B持有条件2等待1)
  3. 不可剥夺条件,在这个线程主动释放之前条件不被其他线程获取
  4. 环状链条件,在发生死锁的时候,两个线程顺序构成了环状链

怎么避免死锁

只要破坏上述四个条件中的一个,就可以避免死锁。一般采用资源有序分配法。也就是对于获取同样临界区资源的AB两个线程,都是先获取1资源再获取2资源。保证这个顺序一致

银行家算法

银行家算法是一种有效避免死锁的算法,其要求如下:

  • 总量约束:一个进程的最大需求量不能超过系统拥有的总资源量
  • 可分期请求:一个进程可以分期请求资源,但需要满足总量约束
  • 推迟分配:当系统拥有的资源小于进程需求的时候,可以延迟分配资源给进程,让进程在有限的时间内获取资源

银行家算法的思想就是:在分配资源给进程之前,检查分配之后的安全性,通过预执行判断是否会产生死锁。如果系统能满足该进程的资源要求,则分配,否则等待 不断检查剩余可用资源是否满足某个最大线程的最大需求,并将满足的进程加入安全序列,回收该进程所持有的资源。重复这个过程,直到所有进程都被加入安全序列。安全状态一定不会发生死锁,不安全状态不一定会发生死锁。