锁知识整理(一)

锁的目的是为了保证并发线程(或者并发访问)操作临界资源的正确性。我们需要了解不同锁的特性和应用场景才能在使用时得心应手。

本文主要介绍应用程序的锁、分布式锁以及MySQL相关的锁,其中MySQL 在下篇文章中介绍。

应用程序锁

互斥锁

互斥锁是一种独占锁,同一个时刻只能被一个线程占用,其他等待的线程阻塞,直到占有锁的线程释放锁。对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程。

成本:两次线程上下文切换的成本

自旋锁

自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。同时CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。

成本:自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源。

注:使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。这里的「忙等待」可以用 while 循环等待实现。

读写锁

读写锁是由读锁和写锁组成。对同一个临界资源,多个线程可以共享读锁,但写锁必须互斥。

1.读优先锁

工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。这种模式可能导致写线程饿死。

2.写优先锁

工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。这种模式可能导致读线程饿死。

3.公平读写锁

公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。

优势:读写锁在读多写少的场景,能发挥出优势。

悲观锁

多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。这只是一种做事的心态。

乐观锁

乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

死锁

死锁产生的原因是两个线程彼此都在等待对方锁住的资源。

Thread 1 locks A, waits for B
Thread 2 locks B, waits for A

解决死锁的方法:超时或死锁检测。

分布式锁

分布式锁主要的特性有互斥性、超时释放、可重入性、高性能和高可用。

基于 Redis 的分布式锁

redis 保证了命令执行的原子性。

1.setnx

setnx 的特性是当 key 不存在时设置成功,否则设置失败,同时可以对这个 key 设置有效期。通过这两个特性以及 redis 命令执行的原子性,可以实现分布式锁的互斥性、超时释放、可重入性。

可能存在的问题:

(1) 如果超时时间设置的不合理会出现一些问题,比如业务执行的时间超过设置的超时时长。

     假如当前有 A,B,C 三个线程,A 占用临界资源 doKey  

     A  线程执行业务,执行时间>超时时间,redis 删除 doKey。

     B 线程先占用 doKey,开始执行业务,此时 A 线程结束业务,释放 doKey

     C 线程占用 doKey,开始执行业务。

     从上面的场景张,B,C 线程本来应该互斥,但因为 doKey 超时时间设置的不合理,导致 B,C在同一时刻同时存在。

    当然如果超时时间设置的太长,如果 A 业务中途因为什么原因导致被 kill 掉,那么B,C  必须等到 doKey 超时,才能执行。

  解决方法:在获取锁时,可以设置 value 为一个随机数,在释放锁时进行读取和对比,确保释放的是当前线程持有的锁,一般是通过 Redis 结合 Lua 脚本的方案实现。

(2)集群环境下,Redis 通过主从复制来实现数据同步,Redis 的主从复制(Replication)是异步的,所以单节点下可用的方案在集群的环境中可能会出现问题,在故障转移(Failover) 过程中丧失锁的安全性。

2.redlock

   redlock 主要解决单点问题,简单来说,单点不可靠,所以获取锁需要从n节点中获取,并且要保证获得锁的数量> (n+1)/2,才认为锁是获取成功的(具体详细请自行搜索)。

基于 Zookeeper 来实现分布式锁

ZK 中分布式锁主要就是靠创建临时的顺序节点来实现的。

1.顺序节点实现的分布式锁:非顺序节点的话,每进来一个线程进来都会去持有锁的节点上注册一个监听器,容易引发“羊群效应”。而顺序节点当发现已经有线程持有锁后,会向它的上一个节点注册一个监听器,这样当持有锁的节点释放后,也只有持有锁的下一个节点可以抢到锁,相当于是排好队来执行的,降低服务器宕机风险。

2.临时节点实现的分布式锁:使用临时节点和 Redis 的过期时间一个道理,就算 ZK 服务器宕机,临时节点会随着服务器的宕机而消失,避免了死锁的情况。

3.ZK(顺序节点) 实现分布式锁的主要流程:

(1)当第一个线程进来时会去父节点上创建一个临时的顺序节点。

(2)第二个线程进来发现锁已经被持有了,就会为当前持有锁的节点注册一个 watcher 监听器。

(3)第三个线程进来发现锁已经被持有了,因为是顺序节点的缘故,就会为上一个节点去创建一个 watcher 监听器。

(4)当第一个线程释放锁后,删除节点,由它的下一个节点去占有锁。

Redis& ZK

1.实现方式的不同,Redis 实现为去插入一条占位数据,而 ZK 实现为去注册一个临时节点。

2.遇到宕机情况时,Redis 需要等到过期时间到了后自动释放锁,而 ZK 因为是临时节点,在宕机时候已经是删除了节点去释放锁。

3.Redis 在没抢占到锁的情况下一般会去自旋获取锁,比较浪费性能,而 ZK 是通过注册监听器的方式获取锁,性能而言优于 Redis。

总结

   本文主要介绍了应用程序锁(读写锁、互斥锁、自旋锁、乐观锁、悲观锁) 和分布式锁(基于redis 和 zk)。上文内容主要参考网上大佬的文章整理。

参考

1.https://mp.weixin.qq.com/s/B338aVa3bPTVogT17f5Huw

2《Zookeeper 分布式一致性原理与实践》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值