1. 互斥锁和自旋锁
区别:
互斥锁:在加锁失败后,线程会释放 CPU,给其他线程;
互斥锁在加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,这样就会造成一定的性能开销。
线程在两次状态的切换中,会有两次线程上下文切换。线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
- 加锁失败时,将 CPU 让给其他线程
- 锁被释放时,线程会转换为[就绪]状态,内核会在合适时间将 CPU 切换回该线程
有大佬统计过,大概在几十纳秒到几微秒之间。如果你能确定被锁住的代码执行时间很短,就应该选用自旋锁,否则使用互斥锁。
自旋锁:在加锁失败后,线程会忙等待,直到它拿到锁;
自旋锁是通过 CAS(Compare And Swap)函数,在用户态完成加锁和解锁,所以不会上下文切换的开销,相对于互斥锁更快,开销更小。
2. 读写锁
读锁是互斥锁,而写锁是共享锁,可以多个线程持有读锁,所以读写锁在读多写少的场景中更能发挥出优势
- 读优先锁
在有线程持有读锁的情况下,当其他线程想获取读锁时,会持续获取读锁,写锁请求等待;当所有读锁释放时,写锁将会被持有。当持续有线程获取读锁,会导致写线程饥饿
- 写优先锁
在有线程持有读锁的情况下,当其他线程想获取写锁时,会优先等待现有的读锁释放并获取写锁,并阻塞后来的读锁请求, 直到写锁释放。当一直有线程获取写锁,就会导致读线程饥饿
- 公平读写锁
用队列把获取锁的线程排队,按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。
3. 乐观锁
先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
4. 分布式锁(多个服务竞争同个资源)
setnx or (set )
- 加锁
1
2
3
4
5
6使用setnx
setnx lock_name uid
expire lock_name ttl
使用set + nx
set lock_name uid ex ttl nxttl 生命周期(毫秒)
使用
setnx
进行了两步操作,非原子操作,可能会出现死锁的情况,所以建议使用set
+nx
。- 解锁
1
2
3
4
5if 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 索引
- 向表中的唯一索引列插入 lock_name,成功则获得锁成功,反之失败
- 通过 delete 语句删除 lock_name
redisson 等等