临界区
临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。只能被单一线程访问的设备,例如:打印机。
锁
当多个线程访问临界区资源时,可能会引起冲突。那么我们需要实现以下效果
- 同一时间内,仅有一个线程可以访问临界区
- 具备可重入性
同时为了防止死锁,我们需要
- 具备锁失效机制
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。一般情况下在尝试一段时间后返回失败
java 线程锁
在了解分布式锁之前我们先了解一下java
的两种锁的实现
synchronized
1 | Object lock = new Object(); |
查看上述代码的字节码
1 | 11: monitorenter |
我们可以观察到在进入synchronized
代码块时,使用monitorenter
指定获取锁,在结束代码块的语句时,使用monitorexit
,释放锁。
等待锁的过程
参考java数组中关于对象头的部分,对象在有锁的情况下,会保存占用锁的线程指针。从而保证在同一个时刻,仅允许一个线程可以占用monitor
锁,无法获取monitor
锁的线程被阻塞直到对象释放锁。
AbstractQueuedSynchronizer
以ReentrantLock
为代表的同步锁框架是基于AQS
,AQS
依赖FIFO
队列实现。线程获取锁通过tryAcquire
向FIFO
插入并使用乐观锁CAS
的方式,判断是否为HEAD
节点,若是HEAD
节点则表示获取到锁,释放锁则通过tryRelease
该队列的HEAD
节点,并唤醒等待节点。
JVM 源码分析之 Object.wait/notify 实现
分布式锁
构建测试程序
在分布式场景下,无法通过JVM
内存来保持一致性。我们需要一个定义一个临界区。对于锁的实现,和java
锁没有本质区别,仅在于实现方式不同
首先我们构造测试程序,我们使用SpringBoot
构建项目
pom
依赖
1 |
|
配置文件
1 | spring: |
临界资源
1 | package com.leaderli.demo.lock; |
锁接口
1 | package com.leaderli.demo.lock; |
为了使代码清晰,封装一些方法
1 | package com.leaderli.demo.util; |
测试程序
1 | package com.leaderli.demo.lock; |
通过使用SpringBoot
的注解ConditionalOnProperty
来方便测试 首先我们测试没有锁的情况下
1 | package com.leaderli.demo.lock; |
配置文件中将NoLock
类激活,即lock.use=nolock
我们很容易就观察到没有锁时,一个线程在运行期间,临界区资源被修改了
数据库锁
根据锁的原则,我们使用数据库锁需要考虑的实现方式:
- 我们通过数据库唯一约束(主键约束)来确保同一时间内,仅有一个线程可以访问临界区资源。通过唯一
ID
,获取锁时插入锁,释放锁则删除锁记录 为了确保可重入性,我们需要记录占用锁的线程,同时为了保证仅在所有重入锁都释放后再释放锁,我们需要记录重入锁的次数,释放锁时判断重入锁次数是否为
0
,为0
则删除锁记录为了具有锁失效机制,当线程占用锁后出现异常情况,没有释放锁,会导致锁一直被占用,其他线程无法获取锁。所以我们需要记录获取锁的时间,以及设定锁的失效时间。则在获取锁时判断是否锁已经失效,失效则删除锁。
为了具有非阻塞锁特性,在线程获取锁时,首先查询数据库锁是否被占用,若被占用,则重新尝试,达到指定次数后,则直接返回获取锁失败。通过
insert
插入锁失败,也直接返回失败
redis
锁
--
zookeeper
锁
--