点赞再看,已成习惯。
序言
我们在前面的文章中详细介绍了 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()
这里主要是实现一个默认的超时等待,基本上是通用的。
前面实现 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 owner =new AtomicReference<>();}
加锁
lock 就是一个不断尝试获取锁的方法,直到成功为止才返回。
@Overridepublic void lock() { // 循环等待,直到获取到锁 while (!tryLock()) { }}@Overridepublic boolean tryLock(String key) { Thread current = Thread.currentThread(); // CAS return owner.compareAndSet(null, current);}
tryLock() 的实现也比较简单,就是通过 CAS 设置持有者为当前线程。
owner 是通过 AtomicReference 声明,保证 CAS 操作的原子性。
解锁实现
解锁的就是一个逆过程,不过这里我们没有做重试,只比较了一次。
通过 CAS,只有当 owner 的持有者为当前线程,且设置为 null 成功时,才返回 true。
释放锁失败,此处直接报错。
@Overridepublic 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: 12first-lock: 14first-lock: 13second-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 owner =new AtomicReference<>(); /** * 计数统计类 * * @since 0.0.2 */ private AtomicLong count = new AtomicLong(0);}
加锁
lock 时直接重复调用 tryLock 方法,直到加锁成功为止。
@Overridepublic void lock() { // 循环等待,直到获取到锁 while (!tryLock()) { // sleep DateUtil.sleep(1); }}@Overridepublic 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。
@Overridepublic 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: 12first-lock: 14first-lock: 13second-lock: 12second-unlock: 12first-unlock: 12second-lock: 13second-unlock: 13first-unlock: 13second-lock: 14second-unlock: 14first-unlock: 14
这样就可以全部正常执行完成了。
小结
前面我们将结果可重入锁的源码,jdk 中的实现更加严谨,同时也更加复杂。
我们文中做了简单的实现,主要是为了让读者更简单的理解整体的逻辑和思想。
这里留一个思考题,如何使用 wait+notify 实现一个可重入的自旋锁?有思路的小伙伴可以在评论区写下自己的想法。
希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。
各位极客的点赞收藏转发,是老马持续写作的最大动力!