各种各样的锁
线程之间的锁
可基于某个共享变量实现。也可使用下面介绍的互斥锁
、自旋锁
等实现
进程之间的锁
分同一台机器和不同机器。同一机器则同样可使用下面介绍的锁实现。不同机器则需使用分布式锁实现。
进程与锁不在同一台机器上(分布式锁),还需要注意网络传输的时间,因此需要判断传输时间,若传输时间大于锁的有效时间,则该锁无效。
互斥锁
当任务1无法获取锁时,则会进入睡眠态,当锁被释放后,任务1会被唤醒并尝试获取锁,因从用户态(尝试获取锁)进入内核态(无法获取锁)存在切换,
因此较耗性能,通常在几十纳秒到几微秒之间
自旋锁
当任务1无法获取锁时,则会不断进行尝试而不进入睡眠态,因此不会进入内核态从而其性能较互斥锁
更好。
一般使用WHILE
等循环语句实现忙等待
,当然这会导致CPU空转从而浪费电能,可以使用CPU的PAUSE
指令实现忙等待
从而节约电能
当任务耗时较短时,使用
自旋锁
比互斥锁
更好,因为互斥锁
的状态切换耗时可能多于任务本身的耗时
读写锁
原理
- 当
读锁
没被任务持有时,多个任务能够同时都获取读锁(并发) - 但当
写锁
被某任务持有后,读任务获取读锁的操作会被阻塞,且其他写任务获取写锁的操作也会被阻塞。
所以,写锁
是排斥锁(X锁),而读锁
则是共享锁(S锁)
当然,根据需要,还会分读优先锁
和写优先锁
读优先锁
任务1对资源上读锁,此时任务2需上写锁但无法获取,后续任务3需上读锁,此时任务3可获取读锁,待任务1、任务3、其他拥有读锁的任务都释放读锁后,任务2才能获取写锁。
此为读优先锁,该锁有问题就是会造成写饥饿
,即一切有任务获取读锁,导致写任务无法获取写锁,从而导致无法执行写操作。
写优先锁
任务1对资源上读锁,此时任务2需写锁但无法获取则阻塞等待,后续任务3需上读锁,但同样也无法获取读锁从而阻塞等待。
待任务1释放读锁后,唤醒任务2使其获取写锁,若后续有任务4获取写锁,则当任务2释放写锁时,任务3仍未能获取读锁,而是唤醒任务4获取写锁。
因此也会导致饥饿
的现象读饥饿
- 既然都有
饥饿
现象,那么就来个公平锁,谁都不偏袒
公平锁
无论读锁还是写锁,一律按照先来后到,排队等待锁,连续几个读锁请求则可一起共同获取读锁,但是在写锁请求后的读锁请求,则排队等待写锁释放后,才能获取读锁。
这样既可以避免饥饿
现象,也可以实现读的并发访问
互斥锁
和自旋锁
都是最基本的锁,读写锁
可以根据场景来选择这两种锁其中的一个进行实现。
悲观锁
悲观锁是指获取或使用一切资源前都认为该资源竞争激烈,需先对资源上锁再使用资源。如上所述的互斥锁
、自旋锁
和一切其他的锁
都属于悲观锁
的范畴,所以悲观锁
其并发较差。
乐观锁
相对的,乐观锁就是认为资源竞争并不激烈,因此总是在最后一步才上锁。比如数据更新时使用的版本号
手段。相对的,乐观锁并发较好
竞争程度不激烈,资源被访问频率不高,则可以使用乐观锁
分布式锁
锁与任务不在同一机器上,通常锁在另外单独的一台机器上,同多台机器上的多个不同进程访问。
根据严格程度又分为多种不同的实现方式。
一些锁的具体实现方法
使用Redis在内存存放一个标识以实现锁(分布式锁)
使用Redis的string实现:
加锁
- 进程生成唯一值作为当前进程的ID(下述用进程ID指代)
- 以锁名为key,进程ID为值存入string
1 | set lock1 [proc_id] NX EX 30 //当lock1不存在时,设置lock1为key,proc_id为value的string,且有效时间为30s |
解锁
- 涉及两步,第一步判断解锁进程是否是加锁进程,可通过进程ID识别
- 删除指定锁名的key
因为涉及两个操作,可在进程内的代码层面进行判断+删除,也可以使用Redis提供的可执行LUA脚本处理
可基于MySQL乐观锁实现(分布式锁)
可参考MySQL乐观锁的实现方式,这里不累述
可重入锁
若此时,加锁进程需要再次进入锁,则需引入可重入锁
1.使用Redis的Hash替换String,将锁名作为key,进程ID作为field,第一次上次默认value为1,后面每加一次锁则value自增加1,如
1 | lock1: |
再次加锁
1 | lock1: |
解锁要执行相应次数,当value为0时,则删除该key完成解锁操作。
联锁
在多个不同的redis实例上获取到锁,把这些锁看作一个锁称为「联锁」。当全部redis实例的锁都获取到后才认为本次联锁成功获取。
要获取多个redis实例上的锁目的是实现分布式锁的高可用,防止一个redis实例挂掉后,在其获取的锁被认为释放了。(当然你可能会认为可采用主从结构避免,但是也会存在slave来不及同步master时,master就挂了,此时slave当上master后,其他尝试获取锁的进程就会获得锁,导致两个进程对同一资源进行操作)
在获取多个锁的过程,要注意设置最大时间,以防止最后一个锁还没获取到时,第一个获取的锁就已失效。
有一个锁实例获取锁失败,则已获取锁的实例全部都要释放锁,重新进行获取操作,同理超时也是。
RedLock锁
基于redlock算法实现分布式高可用锁。与联锁类似,但是无需全部redis实例都获取到锁才算成功,只需要获到的锁的redis实例数量为redis锁实例总数的一半加一个即成功,即n/2+1。