lock
lock方法其实是重写RLock的
对外提供的Lock方法有两种
-
默认的无参lock方法,无参的就是使用默认的过期时间,也就是30S
-
带有过期时间和时间单位参数的lock方法
具体调用的lock方法,可以看到这是一个private方法,需要给三个参数
-
leaseTime:过期时间
-
TimeUnit:时间单位
-
interruptiably:是否优先响应中断,线程是可以将其他线程设为中断状态(interrupted),而在分布式中,不同实例之间的线程不能通信,所以需要借助Redis的发布订阅功能来实现通信,也就是线程在频道中发布让那个线程interrupted(实现的本质是AQS)
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
//获取当前线程ID
long threadId = Thread.currentThread().getId();
//尝试进行加锁
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
//如果加锁成功,return结束
if (ttl == null) {
return;
}
//加锁失败,为了避免空转,通过订阅频道看能不能获取锁
//所以要先进行订阅频道
//订阅频道
RFuture future = subscribe(threadId);
//判断是否优先响应中断
if (interruptibly) {
//优先响应中断并开启开门狗
commandExecutor.syncSubscriptionInterrupted(future);
} else {
//不优先响应中断,并开启看门狗
commandExecutor.syncSubscription(future);
}
//开始进行自旋加锁
try {
//死循环
while (true) {
//再次进行加锁
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
//加锁成功,break跳出循环
if (ttl == null) {
break;
}
//加锁失败,并且返回的锁的过期时间>=0
if (ttl >= 0) {
try {
//锁拥有过期时间,使用AQS来避免空转和判断线程是否被中断了
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
//假设线程处于中断状态,且优先处理中断
if (interruptibly) {
//抛出异常
throw e;
}
//假设线程处于中断状态,但并不优先处理中断
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
//ttl < 0代表锁已经消失
//只需要判断是否优先处理中断
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
//最终拿到锁就退出订阅的频道
//进行退订操作
unsubscribe(future, threadId);
}
}
从这源码上,可以看到大概的逻辑如下
-
尝试进行加锁
-
假如第一次加锁成功,直接return
-
假如第一次加锁失败(锁在Redis存在,且不是自己的锁)
-
订阅频道,猜测可能是为了避免空转
-
开始自旋去获取锁
-
每次自旋获取锁成功就直接break跳出自旋循环
-
每次自旋如果获取锁失败,就会执行future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS)方法
-
个人猜测那个方法是用来避免空转的
tryAcquire
通过上面的代码可以知道,tryAcquire是去获取锁的,下面就看看这个tryAcquire做了什么
先去执行了tryAcquireAsync方法,然后再执行get方法
tryAcquireAsync
源码如下
private RFuture tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
//定义一个RFuture,其实RFuture可以理解成是客户端,里面可以保存Redis执行命令的结果
RFuture ttlRemainingFuture;
//如果锁的释放时间为-1,代表使用默认值
if (leaseTime != -1) {
//执行自定义过期时间去获取锁方法
//将返回的结果存入RFuture中
//给到的参数有等待时间、释放时间、时间单位,线程ID和Reids的EVAL命令
//eval命令就是用来执行lua脚本的
//这里要注意,Redisson将所有Redis的命令存进了RedisCommands里面
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//如果为-1,代表使用默认的过期时间
//使用默认的加锁方法
//同理将结果存入RFuture中
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
//处理一下获取的结果
//lambda表达式,参数为一个BiConsumer接口,消费者接口(没有返回值,有参数,用来对参数进行处理的)
//并且为BiConsumer,可以提供两个参数进行消费处理(这里给了一个)
//onComplete方法就是开启了watch dog
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
//这下面就是接口的accept方法逻辑
//假如e不为空,直接返回
//代表获取失败,并且是出现了异常!!!
//因为e是一个异常
if (e != null) {
return;
}
// lock acquired
//假如没有异常且ttlRemainning为null,代表获取锁成功了!!!
if (ttlRemaining == null) {
//判断过期时间是否为默认的,即判断是否为默认配置
if (leaseTime != -1) {
//不是默认配置的话,将过期时间转换为毫秒
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//如果是默认配置的,开启定时任务重置过期时间
scheduleExpirationRenewal(threadId);
}
}
//假如ttlRemainning不为null,代表获取锁失败
});
//做完处理后,返回整个获取的结果
return ttlRemainingFuture;
}
从这步获取锁的代码中可以看到以下点
-
Redisson对于请求Redis得到的结果都封装与RFuture里面
-
判断获取锁时有没有给定过期时间
-
如果没给定过期时间默认为30S
-
有就给定自定义的
-
并且都调用tryLockInnerAsync方法获取锁,执行的Redis命令为EVAL
-
对获取的结果进行处理
-
并且传入一个BiConsumer接口(二元的消费型接口)
tryLockInnerAsync
这个方法是用来尝试获取锁的,也就是这里执行底层的LUA脚本
终于快看到底层了,我都快裂开了。。。。
RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
//执行LUA脚本去获取锁
//传入这里的参数为
//1. waitTime:等待时间,-1代表不会进行等待
//2. leaseTime:释放锁的时间,即锁的过期时间
//3. unit:时间单位
//4. threadId:线程ID
//5. command:传进来的Redis命令,这里传进来的是EVAL,执行LUA脚本的命令
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call(‘exists’, KEYS[1]) == 0) then " +
"redis.call(‘hincrby’, KEYS[1], ARGV[2], 1); " +
"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then " +
"redis.call(‘hincrby’, KEYS[1], ARGV[2], 1); " +
"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
“return redis.call(‘pttl’, KEYS[1]);”,
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
可以看到这个方法,调用了evalWriteAsync方法,不过从这个方法里面,我们可以提出到重要信息
-
执行的脚本文件内容
-
获取当前线程的名称(getLockName方法,可以看到传进来了一个threadId,那就可以判断,锁的名字是根据线程ID来获取的,且这个lockName的组成是由当前线程连接Redis会产生的客户端ID、冒号和threadId组成)
-
具体执行脚本文件是交由evalWriteAsync方法去做
脚本的内容如下,也就是获取锁执行的命令
redis.call(‘exists’, KEYS[1]) == 0) then
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1);
redis.call(‘pexpire’, KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then
redis.call(‘hincrby’, KEYS[1], ARGV[2], 1);
redis.call(‘pexpire’, KEYS[1], ARGV[1]);
return nil;
end;
return redis.call(‘pttl’, KEYS[1]);
下面来这段脚本到底在干什么的,不过首先我们要认识存储锁的底层是怎样的
存储锁的底层其实是一个hash结构,这个结构可以理解成锁,,比如仓库的所有(其key值其实就是执行tryLock方法给的lockName),而里面的键值对代表是哪个线程拥有了这把锁,存取的是线程用锁清空,键值对的key对应的就是上面所说的getKeyName方法返回的值(组成为clusterID+:+线程ID),而value是与可重入锁相关的,这个线程可以对这把锁进行重入加锁
-
先执行exist命令,判断锁是否存在(锁是一个hash对象)
-
如果不存在
-
执行hincrby命令进行+1,代表重入一次
-
执行pexpire命令进行设置超时时间,这里是给hash对象的键值对设置超时时间,也就是锁,而不是整个锁容器上过期时间
-
返回nil,代表加锁成功
-
如果锁存在
-
执行hexists判断是不是发生重入(键值对的key是当前clusterID+:+线程ID,代表哪个线程正在使用这把锁)【前面已经看到getKeyName方法,生产的就是锁的key】,如果线程本身还持有那把锁(hash),那就代表发生重入了
-
执行hincrby命令,代表发生了重入了,重入次数+1
-
执行pexpire命令进行添加超时时间
-
返回nill,代表加锁成功,重入加锁成功
-
假如锁容器存在且对应的锁不存在,代表这个锁已经被人使用了,不能占有,直接返回锁的过期时间,即hash对象的过期时间
这个方法是专门用来执行lua脚本的
protected RFuture evalWriteAsync(String key, Codec codec, RedisCommand evalCommandType, String script, List keys, Object… params) {
//创建CommandBatchService,用来批量去执行命令的
CommandBatchService executorService = createCommandBatchService();
//通过CommandBatchServic去异步执行脚本
//并且将客户端存储的结果封装在RFuture中
RFuture result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);
//ComandBatchService的创建
//假如commandExecutor可以强转成CommandBatchService
//那么上面创建的CommandBatchService就是由commandExecutor强转成的
if (commandExecutor instanceof CommandBatchService) {
return result;
}
//
RPromise r = new RedissonPromise<>();
RFuture<BatchResult<?>> future = executorService.executeAsync();
future.onComplete((res, ex) -> {
if (ex != null) {
r.tryFailure(ex);
return;
}
//
r.trySuccess(result.getNow());
});
return r;
}
从这里可以看到,执行LUA脚本的是由CommandBatchService来负责的,并且一般来说,这个CommandBatchService是由注入的CommandExecutor强转而来,也就是本质是由CommandExecutor来执行的!!!
总结一下加锁过程
把加锁的具体细节都看完了,现在也可以知道为什么前面用long变量接收加锁的结果了,前面产生的一些疑问也随之解开了
-
底层存储锁的本质是一个hash对象,hash对象就代表是一把锁,比如一个仓库的锁,而hash里面的键值对代表就是当前哪个线程获取了这把锁,只能有一个键值对
-
键值对里面的key就代表哪个线程在拥有锁,组成为:clusterID:线程ID
-
键值对里面的value就代表该锁发生重入的次数
-
并且每次重入成功都会延长过期时间
-
底层使用lua脚本来保证命令的原子性
-
执行命令是交由CommandBatchService执行的,其本质是CommandExecutor(先进行强转,强转成功就使用CommandExecutor)
-
加锁失败,返回的是hash的过期时间,也就是锁的过期时间
Watch dog
回到tryAcquireAsync方法中,接下来就是执行ttlRemainingFuture.onComplete方法了(ttlRemainingFuture是封装加锁的结果的)
一直到onComplete方法底层
大概逻辑如下
-
如果请求redis失败,将错误原因传入accept方法,从accept实现的具体细节可以看到,这样就直接return了
-
如果请求redis成功,进行下一步的开始watchDog
scheduleExpirationRenewal
这个方法就是开启watchDog的
源码如下
protected void scheduleExpirationRenewal(long threadId) {
//ExpirationEntry是用来封装被监视的线程的!!!
ExpirationEntry entry = new ExpirationEntry();
//EXPIRATION_RENEWAL_MAP容器是用来存储被监视的锁的
//通过putIfAbsent方法判断容器中是否已经有该锁了
//getEntryName返回的是entryName,组成为custerId:lockName(tryLock给的锁的名字)
//从这里也知道了RedissonBaseLock的entryName是用来映射监视锁容器的对应value的
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
//如果容器中已经存在该锁了
if (oldEntry != null) {
//往旧锁中添加线程ID
oldEntry.addThreadId(threadId);
} else {
//往新锁中添加线程ID
entry.addThreadId(threadId);
try {
//开启线程去进行锁续命
renewExpiration();
} finally {
//最后判断线程是否被中断
if (Thread.currentThread().isInterrupted()) {
//如果被中断,取消锁续命
cancelExpirationRenewal(threadId);
}
}
}
}
总结一下
-
Redisson使用了ConcurrentHashMap去存储正在续命的锁,key为clusterId:lockName
-
在获取锁成功后,如果releaseTime为-1(也就是默认配置),才会去开启WatchDog
-
使用ExpirationEntry去封装锁的信息,过期时间与拥有该锁的线程ID(LinkeHashMap记录,因为要支持可重入,value代表重入次数)
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip1024b 备注Java获取(资料价值较高,非无偿)
总结
对于面试还是要好好准备的,尤其是有些问题还是很容易挖坑的,例如你为什么离开现在的公司(你当然不应该抱怨现在的公司有哪些不好的地方,更多的应该表明自己想要寻找更好的发展机会,自己的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)
Java面试精选题、架构实战文档
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!
[外链图片转存中…(img-J7oprQJX-1711576837085)]
[外链图片转存中…(img-2WPzdsb5-1711576837086)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
[外链图片转存中…(img-9diGU56R-1711576837087)]
[外链图片转存中…(img-BGriAoAt-1711576837087)]
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip1024b 备注Java获取(资料价值较高,非无偿)
[外链图片转存中…(img-6CAztr5S-1711576837088)]
总结
对于面试还是要好好准备的,尤其是有些问题还是很容易挖坑的,例如你为什么离开现在的公司(你当然不应该抱怨现在的公司有哪些不好的地方,更多的应该表明自己想要寻找更好的发展机会,自己的一些现实因素,比如对于我而言是现在应聘的公司离自己的家更近,又或者是自己工作到达了迷茫期,想跳出迷茫期等等)
[外链图片转存中…(img-t8hDB1Tm-1711576837088)]
Java面试精选题、架构实战文档
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!