线程之间的锁

可基于某个共享变量实现。也可使用下面介绍的互斥锁自旋锁等实现

进程之间的锁

分同一台机器和不同机器。同一机器则同样可使用下面介绍的锁实现。不同机器则需使用分布式锁实现。

进程与锁不在同一台机器上(分布式锁),还需要注意网络传输的时间,因此需要判断传输时间,若传输时间大于锁的有效时间,则该锁无效。

互斥锁

当任务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实现:

加锁

  1. 进程生成唯一值作为当前进程的ID(下述用进程ID指代)
  2. 以锁名为key,进程ID为值存入string
1
set lock1 [proc_id] NX EX 30 //当lock1不存在时,设置lock1为key,proc_id为valuestring,且有效时间为30s

解锁

  1. 涉及两步,第一步判断解锁进程是否是加锁进程,可通过进程ID识别
  2. 删除指定锁名的key
    因为涉及两个操作,可在进程内的代码层面进行判断+删除,也可以使用Redis提供的可执行LUA脚本处理

可基于MySQL乐观锁实现(分布式锁)

可参考MySQL乐观锁的实现方式,这里不累述

可重入锁

若此时,加锁进程需要再次进入锁,则需引入可重入锁
1.使用Redis的Hash替换String,将锁名作为key,进程ID作为field,第一次上次默认value为1,后面每加一次锁则value自增加1,如

1
2
3
4
lock1:
{
"123123-random-id":1
}

再次加锁

1
2
3
4
lock1:
{
"123123-random-id":2
}

解锁要执行相应次数,当value为0时,则删除该key完成解锁操作。

联锁

在多个不同的redis实例上获取到锁,把这些锁看作一个锁称为「联锁」。当全部redis实例的锁都获取到后才认为本次联锁成功获取。
要获取多个redis实例上的锁目的是实现分布式锁的高可用,防止一个redis实例挂掉后,在其获取的锁被认为释放了。(当然你可能会认为可采用主从结构避免,但是也会存在slave来不及同步master时,master就挂了,此时slave当上master后,其他尝试获取锁的进程就会获得锁,导致两个进程对同一资源进行操作)
在获取多个锁的过程,要注意设置最大时间,以防止最后一个锁还没获取到时,第一个获取的锁就已失效。
有一个锁实例获取锁失败,则已获取锁的实例全部都要释放锁,重新进行获取操作,同理超时也是。

RedLock锁

基于redlock算法实现分布式高可用锁。与联锁类似,但是无需全部redis实例都获取到锁才算成功,只需要获到的锁的redis实例数量为redis锁实例总数的一半加一个即成功,即n/2+1。