thread.sleep会释放锁吗_java从零开始手写实现 SpinLock 自旋锁及可重入锁

点赞再看,已成习惯。

序言

我们在前面的文章中详细介绍了 jdk 自带的可重入锁使用及其源码。

本节就让我们一起来实现一个可重入锁。

3dcc26c972807af7ef78326ce88d5619.png

思维导图

接口定义

为了便于后期拓展,我们统一定义接口。

接口

继承自 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 的分布式锁时有介绍过。

b5380df01082755c92b29c03bc2c4a3c.png

自旋锁

自旋锁实现

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 实现一个可重入的自旋锁?有思路的小伙伴可以在评论区写下自己的想法。

希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。

各位极客的点赞收藏转发,是老马持续写作的最大动力!

edf4170782dcf2b93ed49c46fc9c3925.png

深入学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值