点赞再看,已成习惯。
序言
我们在前面的文章中详细介绍了 jdk 自带的可重入锁使用及其源码。
本节就让我们一起来实现一个可重入锁。
接口定义
为了便于后期拓展,我们统一定义接口。
接口
继承自 jdk Lock 接口,并且新增了几个常用的方法。
package com.github.houbb.lock.api.core;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* 锁定义
* @author binbin.hou
* @since 0.0.1
*/
public interface ILock extends Lock {
/**
* 尝试加锁
* @param time 时间
* @param unit 当为
* @param key key
* @return 返回
* @throws InterruptedException 异常
* @since 0.0.1
*/
boolean tryLock(long time, TimeUnit unit,
String key) throws InterruptedException;
/**
* 尝试加锁
* @param key key
* @return 返回
* @since 0.0.1
*/
boolean tryLock(String key);
/**
* 解锁
* @param key key
* @since 0.0.1
*/
void unlock(String key);
}
抽象类
为了便于实现,我们统一定义对应的抽象类:
package com.github.houbb.lock.redis.core;
import com.github.houbb.lock.api.core.ILock;
import com.github.houbb.lock.redis.constant.LockRedisConst;
import com.github.houbb.wait.api.IWait;
import com.github.houbb.wait.core.Waits;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
/**
* 抽象实现
* @author binbin.hou
* @since 0.0.1
*/
public abstract class AbstractLock implements ILock {
/**
* 锁等待
* @since 0.0.1
*/
private final IWait wait;
public AbstractLock() {
this.wait = Waits.threadSleep();
}
protected AbstractLock(IWait wait) {
this.wait = wait;
}
@Override
public void lock() {
throw new UnsupportedOperationException();
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException();
}
@Override
public boolean tryLock() {
return tryLock(LockRedisConst.DEFAULT_KEY);
}
@Override
public void unlock() {
unlock(LockRedisConst.DEFAULT_KEY);
}
@Override
public boolean tryLock(long time, TimeUnit unit, String key) throws InterruptedException {
long startTimeMills = System.currentTimeMillis();
// 一次获取,直接成功
boolean result = this.tryLock(key);
if(result) {
return true;
}
// 时间判断
if(time <= 0) {
return false;
}
long durationMills = unit.toMillis(time);
long endMills = startTimeMills + durationMills;
// 循环等待
while (System.currentTimeMillis() < endMills) {
result = tryLock(key);
if(result) {
return true;
}
// 等待 1ms
wait.wait(TimeUnit.MILLISECONDS, 1);
}
return false;
}
@Override
public synchronized boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return tryLock(time, unit, LockRedisConst.DEFAULT_KEY);
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
这里主要是实现一个默认的超时等待,基本上是通用的。
前面实现 redis 的分布式锁时有介绍过。
自旋锁实现
java 实现
类定义
我们直接继承自 AbstractLock
抽象类。
package com.github.houbb.lock.redis.core;
import com.github.houbb.lock.redis.exception.LockRuntimeException;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋锁
* @author binbin.hou
* @since 0.0.2
*/
public class LockSpin extends AbstractLock {
/**
* volatile 引用,保证线程间的可见性+易变性
*
* @since 0.0.2
*/
private AtomicReference<Thread> owner =new AtomicReference<>();
}
加锁
lock 就是一个不断尝试获取锁的方法,直到成功为止才返回。
@Override
public void lock() {
// 循环等待,直到获取到锁
while (!tryLock()) {
}
}
@Override
public boolean tryLock(String key) {
Thread current = Thread.currentThread();
// CAS
return owner.compareAndSet(null, current);
}
tryLock() 的实现也比较简单,就是通过 CAS 设置持有者为当前线程。
owner 是通过 AtomicReference 声明,保证 CAS 操作的原子性。
解锁实现
解锁的就是一个逆过程,不过这里我们没有做重试,只比较了一次。
通过 CAS,只有当 owner 的持有者为当前线程,且设置为 null 成功时,才返回 true。
释放锁失败,此处直接报错。
@Override
public void unlock(String key) {
Thread current = Thread.currentThread();
boolean result = owner.compareAndSet(current, null);
if(!result) {
throw new LockRuntimeException("解锁失败");
}
}
测试
自旋锁可以说是最简单的锁实现了,我们一起看一下实现的是否符合预期。
线程定义
package com.github.houbb.lock.test.core;
import com.github.houbb.lock.api.core.ILock;
import com.github.houbb.lock.redis.core.LockSpin;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class LockSpinThread implements Runnable {
private final ILock lock = new LockSpin();
@Override
public void run() {
System.out.println("first-lock: " + Thread.currentThread().getId());
lock.lock();
System.out.println("second-lock: " + Thread.currentThread().getId());
lock.lock();
lock.unlock();
System.out.println("second-unlock: " + Thread.currentThread().getId());
lock.unlock();
System.out.println("first-unlock: " + Thread.currentThread().getId());
}
}
测试
public static void main(String[] args) {
final Runnable runnable = new LockSpinThread();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
我们同时开启 3 个线程,执行。
日志输出:
first-lock: 12
first-lock: 14
first-lock: 13
second-lock: 12 // 卡住
我们发现在第二次加锁的时候卡住了,这显然不太符合正常的使用习惯。
因为同一个线程,我们认为已经持有锁之后,重复加锁应该是成功的,这个就叫锁的可重入性。
但是我们的实现太简单粗暴了,我们第一次加所已经将 owner 设置为当前线程了,再次加锁 owner.compareAndSet(null, current);
是无法成功的,因为已经不是预期的 null 值了。
那应该怎么解决呢?
自旋锁的可重入版本
解决思路
我们引入一个计数器。
如果已经是当前线程持有锁,加锁时,计数器直接加1,并返回成功;解锁时,直接减1即可。
java 实现
类定义
和自旋锁类似,我们新增一个计数器变量。
package com.github.houbb.lock.redis.core;
import com.github.houbb.heaven.util.util.DateUtil;
import com.github.houbb.lock.redis.exception.LockRuntimeException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋锁-可重入
* @author binbin.hou
* @since 0.0.2
*/
public class LockSpinRe extends AbstractLock {
/**
* volatile 引用,保证线程间的可见性+易变性
*
* @since 0.0.2
*/
private AtomicReference<Thread> owner =new AtomicReference<>();
/**
* 计数统计类
*
* @since 0.0.2
*/
private AtomicLong count = new AtomicLong(0);
}
加锁
lock 时直接重复调用 tryLock 方法,直到加锁成功为止。
@Override
public void lock() {
// 循环等待,直到获取到锁
while (!tryLock()) {
// sleep
DateUtil.sleep(1);
}
}
@Override
public boolean tryLock(String key) {
Thread current = Thread.currentThread();
// 判断是否已经拥有此锁
if(current == owner.get()) {
// 原子性自增 1
count.incrementAndGet();
return true;
}
// CAS
return owner.compareAndSet(null, current);
}
tryLock 和前面的方法对比,多了一个判断。
如果线程已经拥有此锁,则直接计数器+1,并且返回获取锁成功。
解锁
有借有还,再借不难。
解锁也是类似的操作,如果当前线程已经持有锁,且 count 不是 0,直接返回 true。
@Override
public void unlock(String key) {
Thread current = Thread.currentThread();
// 可重入实现
if(owner.get() == current && count.get() != 0) {
count.decrementAndGet();
return;
}
boolean result = owner.compareAndSet(current, null);
if(!result) {
throw new LockRuntimeException("解锁失败");
}
}
验证
package com.github.houbb.lock.test.core;
import com.github.houbb.lock.api.core.ILock;
import com.github.houbb.lock.redis.core.LockSpin;
import com.github.houbb.lock.redis.core.LockSpinRe;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class LockSpinReThread implements Runnable {
private final ILock lock = new LockSpinRe();
@Override
public void run() {
System.out.println("first-lock: " + Thread.currentThread().getId());
lock.lock();
System.out.println("second-lock: " + Thread.currentThread().getId());
lock.lock();
lock.unlock();
System.out.println("second-unlock: " + Thread.currentThread().getId());
lock.unlock();
System.out.println("first-unlock: " + Thread.currentThread().getId());
}
public static void main(String[] args) {
final Runnable runnable = new LockSpinReThread();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
我们将线程的锁实现换成 LockSpinRe 可重入的自旋锁。
日志输出如下:
first-lock: 12
first-lock: 14
first-lock: 13
second-lock: 12
second-unlock: 12
first-unlock: 12
second-lock: 13
second-unlock: 13
first-unlock: 13
second-lock: 14
second-unlock: 14
first-unlock: 14
这样就可以全部正常执行完成了。
小结
前面我们将结果可重入锁的源码,jdk 中的实现更加严谨,同时也更加复杂。
我们文中做了简单的实现,主要是为了让读者更简单的理解整体的逻辑和思想。
这里留一个思考题,如何使用 wait+notify 实现一个可重入的自旋锁?有思路的小伙伴可以在评论区写下自己的想法。
希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。
各位极客的点赞收藏转发,是老马持续写作的最大动力!