v20230618更新
- 流程概括
流程概括
通过整体的介绍,流程简单概括:
“”
A、B线程争抢一把锁,A获取到后,B阻塞
B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息
A操作完成释放了锁,B线程收到订阅消息通知
B被唤醒开始继续抢锁,拿到锁
详细加锁解锁流程总结如下图:
5. 公平锁
以上介绍的可重入锁是非公平锁,Redisson还基于Redis的队列(List)和ZSet实现了公平锁
公平的定义是什么?
公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少
FairSync
回顾JUC的ReentrantLock公平锁的实现
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
AQS已经提供了整个实现,是否公平取决于实现类取出节点逻辑是否顺序取
AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,通过内置FIFO队列来完成资源获取线程的排队工作,他自身没有实现同步接口,仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用(上图),支持独占和共享获取,这是基于模版方法模式的一种设计,给公平/非公平提供了土壤。
我们用2张图来简单解释AQS的等待流程(出自《JAVA并发编程的艺术》)
一张是同步队列(FIFO双向队列)管理 获取同步状态失败(抢锁失败)的线程引用、等待状态和前驱后继节点的流程图
一张是独占式获取同步状态的总流程,核心acquire(int arg)方法调用流程
可以看出锁的获取流程
AQS维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋,移出队列或停止自旋的条件是前驱节点为头节点切成功获取了同步状态。
而比较另一段非公平锁类NonfairSync
可以发现,控制公平和非公平的关键代码,在于hasQueuedPredecessors方法。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
NonfairSync减少了了hasQueuedPredecessors判断条件,该方法的作用就是
查看同步队列中当前节点是否有前驱节点,如果有比当前线程更早请求获取锁则返回true。
保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则
为什么JUC以默认非公平锁呢?
因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。
可见公平的代价是性能与吞吐量。
Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的。
RedissonFairLock
RedissonFairLock 用法依然很简单
RLock fairLock = redissonClient.getFairLock(lockName);
fairLock.lock();
RedissonFairLock继承自RedissonLock,同样一路向下找到加锁实现方法tryLockInnerAsync。
这里有2段冗长的Lua,但是Debug发现,公平锁的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua较长,参数也多,我们着重分析Lua的实现规则
2.模拟
如果模拟以下顺序,就会明了redisson公平锁整个加锁流程
假设 t1 10:00:00 < t2 10:00:10 < t3 10:00:20
排队结构如图
公平锁的释放脚本和重入锁类似,多了一步加锁开头的清理过期key的while true逻辑,在此不再展开篇幅描述。
由上可以看出,Redisson公平锁的玩法类似于延迟队列的玩法,核心都在Redis的List和zSet结构的搭配,但又借鉴了AQS实现,在定时判断头节点上如出一辙(watchDog),保证了锁的竞争公平和互斥。并发场景下,lua脚本里,zSet的score很好地解决了顺序插入的问题,排列好优先级。并且为了防止因异常而退出的线程无法清理,每次请求都会判断头节点的过期情况给予清理,最后释放时通过CHANNEL通知订阅线程可以来获取锁,重复一开始的步骤,顺利交接到下一个顺序线程。
六、总结
Redisson整体实现分布式加解锁流程的实现稍显复杂,作者Rui Gu对Netty和JUC、Redis研究深入,利用了很多高级特性和语义,值得深入学习,本次介绍也只是单机Redis下锁实现,Redisson也提供了多机情况下的联锁(MultiLock)和官方推荐的红锁(RedLock),下一章再详细介绍。
所以,当你真的需要分布式锁时,不妨先来Redisson里找找。
作者:长江水面写日记
链接:https://juejin.cn/post/6961380552519712798
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
v2021-02-28
多个线程节点锁竞争的正常流程如下图出现节点下线的异常流程如下图:
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
public void tryTest(){
Redisson redisson=null;
RLock redissonLock= redisson.getLock("produce_001");
redissonLock.lock();
//获取到了锁: 维护一个timetask(Timeout scheduledFuture)定时任务进行锁超时,
//未获取到锁的thread: AsyncSemaphore 内部自旋并监听通知其他thread释放获取锁(其他未获取到的thread 会被维护到一个锁存器Latch,当Acquired的thread释放时,
// 会根据公平锁还是非公平锁进行订阅),
redissonLock.unlock();
}