ReentrantLock(本地锁) 和 RedissonLock(Redis) 都是可重进锁,首先他们必须满足以下两点:
- 线程(进程)互斥
- 同线程(进程)可重进
ReentrantLock
java.util.concurrent 包下面的锁,也是我们常用的本地锁。
用法:
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
ReentrantLock 如何实现线程互斥:
ReentrantLock 我们主要看 非公平的,他的主要实现逻辑是在NonfairSync 继承于AbstractQueuedSynchronizer。所以我们看 AbstractQueuedSynchronizer 提供了什么?
AbstractQueuedSynchronizer 代码分析:
上锁方法:
public final void acquire(int arg) {
// 如果没有立马获取锁,什么叫获取锁是交子类去处理的
// NonfairSync 就是把自己线程设置进去,并且把state + 1
if (!this.tryAcquire(arg) &&
// 就把当线程封装成一个节点放到同步等待队列队尾
this.acquireQueued(this.addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
private Node addWaiter(Node mode) {
// 把当前线程封装成一个node
Node node = new Node(Thread.currentThread(), mode);
// ... 性能优化
// 加入到队尾
enq(node);
return node;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 不断循环队列,尝试把自己设置到队头
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 并没有获取到锁,阻塞住,等待唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
解锁方法:
public final boolean release(int arg) {
// 什么叫解锁是交子类去处理的
// NonfairSync 就是把当前线程设置null,并且把state - 1
if (this.tryRelease(arg)) {
AbstractQueuedSynchronizer.Node h = this.head;
if (h != null && h.waitStatus != 0) {
this.unparkSuccessor(h);
}
return true;
} else {
return false;
}
}
======================================================================
NonfairSync 是怎么定义自己的解锁方法:
ReentrantLock.Sync.class
// 如何定义获取锁
final boolean nonfairTryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = this.getState();
if (c == 0) {
// 如果是0,就是获取到锁了,把自己设置进去
if (this.compareAndSetState(0, acquires)) {
// 设置当前锁线程
this.setExclusiveOwnerThread(current);
return true;
}
// 如果就是自己拥有锁,那就把次数加+1
} else if (current == this.getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) {
throw new Error("Maximum lock count exceeded");
}
this.setState(nextc);
return true;
}
return false;
}
// 如何定义释放锁
protected final boolean tryRelease(int releases) {
// 当前获取锁次数减一
int c = this.getState() - releases;
// 只能当前线程去解锁(注意等等redis也有注意的逻辑)
if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
} else {
boolean free = false;
// 如果状态为0就是释放锁了
if (c == 0) {
free = true;
// 释放锁,把当前线程变成 null
this.setExclusiveOwnerThread((Thread)null);
}
// -1
this.setState(c);
return free;
}
}
ReentrantLock 如何实现线程可重进:
为什么锁要可重进?
防止死锁的情况。 假设我们的锁是不可冲进的,比方 a() b() 用的是同一个锁,我们在a() 调用了 b() ,这个时候 a先获取锁没有释放又调用b() 也要获取锁。a() 在等待 b() 执行完,b() 在等待 a() 释放锁。这样就是死锁了
解决方法: 在上面代码可见,在lock() 方法当前线程可直接获得锁向state+1,在unlock时候如果不是当前线程直接进入等待状态,等待别的线程把state减到0才唤醒。
ReentrantLock 总结:
上锁: 每当有线程请求锁,如果是当前线程拥有锁就 state +1,如果不是就当前线程进入阻塞状态,进入等待队列等待唤醒。
解锁: 当有线程请求解锁,如果是当前线程就 state -1,如果state 减到等于0,那就放开当前线程。唤醒 等待队列的线程。(非当前线程不能解锁)
======================================================================
RedissonLock:
基于Redis的Redisson分布式可重入锁 RLock Java对象实现了java.util.concurrent.locks.Lock 接口。
用法:
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
首先 Redis 并不提供锁给我们,Redisson是在 Redis这个内存数据库上以自己的方式实现了分布式锁。
我们先理解一下分布式锁
现在分布式锁基本上有三种实现:基于数据库、基于Zookeeper调度中心,基于Redis。
分布式锁的必要条件:多进程可见、互斥、可重进。
RedissonLock 如何实现多进程可见:
Redis是独立JVM之外的,因此本身满足多进程可见的要求。
RedissonLock 如何实现进程(线程)互斥:
互斥:同一时间只有一个进程(线程)获取锁标记。
这个好办,我们跟 ReentrantLock 一样就好了,全部服务器共享同一个属性值(1)。谁获取了就把他设置成1。用 redis 的 setnx key value 就好了。
127.0.0.1:0>setnx lock 1
"1"
# 如果存在,再次设置会返回0
127.0.0.1:0>setnx lock 1
"0"
# 获取lock
127.0.0.1:0>get lock
"1"
跟本地不同的是我这个值是全部服务器的,如果如果获取锁的服务器突然崩溃了。那其他服务器永远获取不了这把锁了。
有人说 finally unlock可以。但是你要知道崩溃的时候根本走不到 finally 的。
解决方案:
我们需要为每个锁做一个保底,就算服务器崩溃了。锁也得自动解除。 我们为刚刚那个 “属性” 设置一个时间。
set lock 1 EX 60 NX SET KEY VALUE EX [seconds] PX [milliseconds] NX XX
# EX seconds – 设置键key的过期时间,单位时秒
# PX milliseconds – 设置键key的过期时间,单位时毫秒
# NX – 只有键key不存在的时候才会设置key的值
# XX – 只有键key存在的时候才会设置key的值
RedissonLock 如何实现进程(线程)可重进:
在上面看到,其实可重进有两条重要的原则的: 如果是当前线程再次获取锁,无需等待直接进入 非当前线程无法删除当前线程的锁
我们还是看看上面使用了 set lock 1 EX 60 NX 之后还有什么问题吧。
- 假设3个进程:A/B/C,在执行任务,并争锁,此时A获取了锁,并且设置自动过期时间为60s
- A开始执行业务,因为是某种原因,业务阻塞,耗时60s,此时锁自动释放了
- B恰好此时开始尝试获取锁,因为锁已经自动释放,成功释放锁
- 说A此时业务执行完毕,执行释放锁逻辑(删除 key),于是B的锁已经被释放,而B其实还在执行业务此时进程C尝试获取锁,也成功了,因为A把B的锁删除了。
这个时候就违反了我们进程互斥的规则了。
解决方法: 解锁的时候必须是自己的锁才能解锁,否则不能解除。 在上锁的时候把 “属性”设置为唯一id,解锁的时候判断是否是跟自己同样的唯一id,否则不能解锁。
// 上锁伪代码
boolean lock(string key,string uuid,long time){
while(true){
string result = setRedisValue(key,uuid,time);
if ("OK".equals(result)) {
return true;
}
}
}
// 解锁伪代码
boolean unlock(string key,string uuid){
string value = getRedisValue(key);
if(!value.equals(uuid)){
return false;
}
long result = delRedisValue(key);
return 1 == result ? true: false;
}
这样就解决上述问题了。但这样仍然会遇到 ReentrantLock 死锁 的情况。那么 RedissonLock 是怎么解决这个问题的呢?
RedissonLock 有完整的方案,我们直接跳到 RedissonLock 的源码看。
思路如下:(不想看源码直接看思路就可以了)
获取锁的步骤:
1、判断lock是否存在 EXISTS lock
2、不存在,则自己获取锁,记录重入层数为1.
2、存在,说明有人获取锁了,下面判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
3、不存在,说明锁已经有了,且不是自己获取的,锁获取失败.
3、存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20
释放锁的步骤:
1、判断当前线程id作为hashKey是否存在: HEXISTS lock threadId
2、不存在,说明锁已经失效,不用管了
2、存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,
3、获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock
RedissonLock 构造方法
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
// 重点:连接id
this.id = commandExecutor.getConnectionManager().getId();
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
RedissonLock 上锁代码:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
try {
// 不断地循环加锁命令
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// 是否时间到了,释放锁
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
}
} finally {
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), 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(getName()), internalLockLeaseTime, getLockName(threadId));
}
RedissonLock 发送的锁脚本
local key = KEYS[1]; -- 第1个参数,锁的key
local threadId = ARGV[1]; -- 第2个参数,连接id + 线程唯一标识
local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间
if(redis.call('exists', key) == 0) then -- 判断锁是否已存在
redis.call('hset', key, threadId, '1'); -- 不存在, 则获取锁
redis.call('expire', key, releaseTime); -- 设置有效期
return 1; -- 返回结果
end;
if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己
redis.call('hincrby', key, threadId, '1'); -- 如果是自己,则重入次数+1
redis.call('expire', key, releaseTime); -- 设置有效期
return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
RedissonLock 解锁方法
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
RedissonLock 发送的解锁脚本
local key = KEYS[1]; -- 第1个参数,锁的key
local threadId = ARGV[1]; -- 第2个参数,连接id + 线程唯一标识
if (redis.call('HEXISTS', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
return nil; -- 如果已经不是自己,则直接返回
end;
local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的锁,则重入次数-1
if (count == 0) then -- 判断是否重入次数是否已经为0
redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除
return nil;
end;
RedissonLock锁总结: RedissonLock 的思路也很简单,用一个map来存锁数据,key是连接id+线程id,value是重进次数。
上锁: 首先,需要判断 “map” 是否存在,如果不存在把自己的线程id作为key,value为1 设置上去。 如果 “map” 不存在就查询 是否是当前进程的线程(threadId)获取锁,如果一致 “map” 的value +1。当然如果不是自己的话就是失败了。
解锁: 首先,需要判断 “map” 是否存在,并且是否是自己获得锁,如果不是那就不能解锁。 如果是自己的话 “map” value -1 。等于0的时候就是释放锁了。以 key 删除。
======================================================================
总结:
ReentrantLock 和 RedissonLock 思路都差不多。都是用一个对全进程(线程) 可见的值 和 标识当前线程 来调度锁的操作。ReentrantLock 因为是本地锁,显然是做了更多的优化的 等待队列 + 线程阻塞和唤醒。RedissonLock 只是利用 Redis 全进程可见,并且是线程安全的这个特性去完成对全进程的加锁操作。
后话: 为什么 RedissonLock 没有 优化的 等待队列 + 线程阻塞和唤醒 这个优化呢?最大的问题还是Redis是远程服务端,一旦我们跟 Redis 失联了,那我们在等待队列的本地线程就永远阻塞了。
特别说明: redis的锁解析很大部分是有参考 《高级Redis应用进阶课 一站式Redis解决方案》 慕课网的课。感谢~ 大家有空也可以看看~