0%

1. 互斥锁和自旋锁

区别:

  • 互斥锁:在加锁失败后,线程会释放 CPU,给其他线程;
    互斥锁在加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,这样就会造成一定的性能开销。
    线程在两次状态的切换中,会有两次线程上下文切换

    线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

    1. 加锁失败时,将 CPU 让给其他线程
    2. 锁被释放时,线程会转换为[就绪]状态,内核会在合适时间将 CPU 切换回该线程

    有大佬统计过,大概在几十纳秒到几微秒之间。如果你能确定被锁住的代码执行时间很短,就应该选用自旋锁,否则使用互斥锁。

  • 自旋锁:在加锁失败后,线程会忙等待,直到它拿到锁;
    自旋锁是通过 CAS(Compare And Swap)函数,在用户态完成加锁和解锁,所以不会上下文切换的开销,相对于互斥锁更快,开销更小。

2. 读写锁

读锁是互斥锁,而写锁是共享锁,可以多个线程持有读锁,所以读写锁在读多写少的场景中更能发挥出优势

  • 读优先锁
    在有线程持有读锁的情况下,当其他线程想获取读锁时,会持续获取读锁,写锁请求等待;当所有读锁释放时,写锁将会被持有。

    当持续有线程获取读锁,会导致写线程饥饿

  • 写优先锁
    在有线程持有读锁的情况下,当其他线程想获取写锁时,会优先等待现有的读锁释放并获取写锁,并阻塞后来的读锁请求, 直到写锁释放。

    当一直有线程获取写锁,就会导致读线程饥饿

  • 公平读写锁
    用队列把获取锁的线程排队,按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。

3. 乐观锁

先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

4. 分布式锁(多个服务竞争同个资源)

  • setnx or (set )

    1. 加锁
    1
    2
    3
    4
    5
    6
    # 使用setnx
    setnx lock_name uid
    expire lock_name ttl

    # 使用set + nx
    set lock_name uid ex ttl nx

    ttl 生命周期(毫秒)

    使用setnx进行了两步操作,非原子操作,可能会出现死锁的情况,所以建议使用set + nx

    1. 解锁
    1
    2
    3
    4
    5
    if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
    else
    return 0
    end

    解锁时为避免非本人解锁,一般将服务的 uid (unique id) 存储在锁的 value 中,解锁是对比 value,防止错误解锁引发事故。
    为保证原子性,所以使用 lua 脚本进行操作。

  • 通过 sql 中的 unique 索引

    1. 向表中的唯一索引列插入 lock_name,成功则获得锁成功,反之失败
    2. 通过 delete 语句删除 lock_name
  • redisson 等等