什么是可重入锁?怎么实现重入锁?下面本篇文章就来带大家深入聊聊redis实现分布式重入锁的方法,希望对大家有所帮助!

什么是不可重入锁?

即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到而阻塞。

什么是可重入锁?

可重入锁,也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。 就是同一个线程再次进入同样代码时,可以再次拿到该锁。

可重入锁作用?

防止在同一线程中多次获取锁而导致死锁发生。

注:在java的编程中synchronized 和 ReentrantLock都是可重入锁。

基于synchronized的可重入锁

步骤1:双重加锁逻辑

public class SynchronizedDemo {
    //模拟库存100
    int count=100;
    public synchronized void operation(){
        log.info("第一层锁:减库存");
        //模拟减库存
        count--;
        add();
        log.info("下订单结束库存剩余:{}",count);
    }

    private synchronized void add(){
        log.info("第二层锁:插入订单");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

步骤2:加个测试类

public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
    for (int i = 0; i < 3; i++) {
        int finalI = i;
        new Thread(()->{
            log.info("-------用户{}开始下单--------", finalI);
            synchronizedDemo.operation();
        }).start();
    }
}

步骤3:测试

20:44:04.013 [Thread-2] INFO com.agan.redis.controller.SynchronizedController - -------用户2开始下单--------
20:44:04.013 [Thread-1] INFO com.agan.redis.controller.SynchronizedController - -------用户1开始下单--------
20:44:04.013 [Thread-0] INFO com.agan.redis.controller.SynchronizedController - -------用户0开始下单--------
20:44:04.016 [Thread-2] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第一层锁:减库存
20:44:04.016 [Thread-2] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第二层锁:插入订单
20:44:14.017 [Thread-2] INFO com.agan.redis.Reentrant.SynchronizedDemo - 下订单结束库存剩余:99
20:44:14.017 [Thread-0] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第一层锁:减库存
20:44:14.017 [Thread-0] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第二层锁:插入订单
20:44:24.017 [Thread-0] INFO com.agan.redis.Reentrant.SynchronizedDemo - 下订单结束库存剩余:98
20:44:24.017 [Thread-1] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第一层锁:减库存
20:44:24.017 [Thread-1] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第二层锁:插入订单
20:44:34.017 [Thread-1] INFO com.agan.redis.Reentrant.SynchronizedDemo - 下订单结束库存剩余:97
  • 由于synchronized关键字修饰的是方法,所有加锁为实例对象:synchronizedDemo
  • 运行结果可以看出减库存和插入订单都是每个线程都完整运行两个方法完毕,才能释放锁,其他线程才能拿锁,即是一个线程多次可以拿到同一个锁,可重入。所以synchronized也是可重入锁。

基于ReentrantLock的可重入锁

ReentrantLock,是一个可重入且独占式的锁,是一种递归无阻塞的同步锁。和synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。

步骤1:双重加锁逻辑

public class ReentrantLockDemo {

    private Lock lock =  new ReentrantLock();

    public void doSomething(int n){
        try{
            //进入递归第一件事:加锁
            lock.lock();
            log.info("--------递归{}次--------",n);
            if(n<=2){
                try {
                    Thread.sleep(1000*2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.doSomething(++n);
            }else{
                return;
            }
        }finally {
            lock.unlock();
        }
    }

}

步骤2:加个测试类

public static void main(String[] args) {
    ReentrantLockDemo reentrantLockDemo=new ReentrantLockDemo();
    for (int i = 0; i < 3; i++) {
        int finalI = i;
        new Thread(()->{
            log.info("-------用户{}开始下单--------", finalI);
            reentrantLockDemo.doSomething(1);
        }).start();
    }
}

步骤3:测试

20:55:23.533 [Thread-1] INFO com.agan.redis.controller.ReentrantController - -------用户1开始下单--------
20:55:23.533 [Thread-2] INFO com.agan.redis.controller.ReentrantController - -------用户2开始下单--------
20:55:23.533 [Thread-0] INFO com.agan.redis.controller.ReentrantController - -------用户0开始下单--------
20:55:23.536 [Thread-1] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归1次--------
20:55:25.537 [Thread-1] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归2次--------
20:55:27.538 [Thread-1] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归3次--------
20:55:27.538 [Thread-2] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归1次--------
20:55:29.538 [Thread-2] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归2次--------
20:55:31.539 [Thread-2] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归3次--------
20:55:31.539 [Thread-0] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归1次--------
20:55:33.539 [Thread-0] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归2次--------
20:55:35.540 [Thread-0] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------递归3次--------
  • 运行结果可以看出,每个线程都可以多次加锁解锁的,ReentrantLock是可重入的。

redis如何实现分布式重入锁?

setnx虽然可以实现分布式锁,但是不可重入,在一些复杂的业务场景,我们需要分布式重入锁时, 对于redis的重入锁业界还是有很多解决方案的,目前最流行的就是采用Redisson。【相关推荐:Redis视频教程】

什么是Redisson?

  • Redisson是Redis官方推荐的Java版的Redis客户端。
  • 基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。
  • 在网络通信上是基于NIO的Netty框架,保证网络通信的高性能。
  • 在分布式锁的功能上,它提供了一系列的分布式锁;如:
    • 可重入锁(Reentrant Lock)
    • 公平锁(Fair Lock)
    • 非公平锁(unFair Lock)
    • 读写锁(ReadWriteLock)
    • 联锁(MultiLock)
    • 红锁(RedLock)

案例实战:体验redis分布式重入锁

步骤1:Redisson配置

Redisson配置的可以查考:redis分布式缓存(三十四)一一 SpringBoot整合Redission - 掘金 (juejin.cn)

https://juejin.cn/post/7057132897819426824

步骤2:Redisson重入锁测试类

public class RedisController {

    @Autowired
    RedissonClient redissonClient;

    @GetMapping(value = "/lock")
    public void get(String key) throws InterruptedException {
        this.getLock(key, 1);
    }

    private void getLock(String key, int n) throws InterruptedException {
        //模拟递归,3次递归后退出
        if (n > 3) {
            return;
        }
        //步骤1:获取一个分布式可重入锁RLock
        //分布式可重入锁RLock :实现了java.util.concurrent.locks.Lock接口,同时还支持自动过期解锁。
        RLock lock = redissonClient.getLock(key);
        //步骤2:尝试拿锁
        // 1. 默认的拿锁
        //lock.tryLock();
        // 2. 支持过期解锁功能,10秒钟以后过期自动解锁, 无需调用unlock方法手动解锁
        //lock.tryLock(10, TimeUnit.SECONDS);
        // 3. 尝试加锁,最多等待3秒,上锁以后10秒后过期自动解锁
        // lock.tryLock(3, 10, TimeUnit.SECONDS);
        boolean bs = lock.tryLock(3, 10, TimeUnit.SECONDS);
        if (bs) {
            try {
                // 业务代码
                log.info("线程{}业务逻辑处理: {},递归{}" ,Thread.currentThread().getName(), key,n);
                //模拟处理业务
                Thread.sleep(1000 * 5);
                //模拟进入递归
                this.getLock(key, ++n);
            } catch (Exception e) {
                log.error(e.getLocalizedMessage());
            } finally {
                //步骤3:解锁
                lock.unlock();
                log.info("线程{}解锁退出",Thread.currentThread().getName());
            }
        } else {
            log.info("线程{}未取得锁",Thread.currentThread().getName());
        }
    }
}

RLock三个加锁动作:

    1. 默认的拿锁
    • lock.tryLock();
    1. 支持过期解锁功能,10秒钟以后过期自动解锁
    • lock.tryLock(10, TimeUnit.SECONDS);
    1. 尝试加锁,最多等待3秒,上锁以后10秒后过期自动解锁
    • lock.tryLock(3, 10, TimeUnit.SECONDS);

区别:

  • lock.lock():阻塞式等待。默认加的锁都是30s
    • 锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长而导致锁自动过期被删掉(默认续期)
    • 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
    • lock()如果我们未指定锁的超时时间,就使用【看门狗默认时间】: lockWatchdogTimeout = 30 * 1000
    • 原理:只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
  • lock.lock(10,TimeUnit.SECONDS) :10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
    • 出现的问题:在锁时间到了以后,不会自动续期
    • 原理:lock(10,TimeUnit.SECONDS)如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们制定的时间

最佳实战:

  • lock.lock(10,TimeUnit.SECONDS); 省掉看门狗续期操作,自动解锁时间一定要大于业务执行时间,手动解锁

步骤3:测试

访问3次:http://127.0.0.1:9090/lock?key=ljw

线程http-nio-9090-exec-1业务逻辑处理: ljw,递归1
线程http-nio-9090-exec-2未取得锁
线程http-nio-9090-exec-1业务逻辑处理: ljw,递归2
线程http-nio-9090-exec-3未取得锁
线程http-nio-9090-exec-1业务逻辑处理: ljw,递归3
线程http-nio-9090-exec-1解锁退出
线程http-nio-9090-exec-1解锁退出
线程http-nio-9090-exec-1解锁退出

通过测试结果:

  • nio-9090-exec-1线程,在getLock方法递归了3次,即证明了lock.tryLock是可重入锁
  • 只有当nio-9090-exec-1线程执行完后,io-9090-exec-2 nio-9090-exec-3 未取得锁 因为lock.tryLock(3, 10, TimeUnit.SECONDS),尝试加锁,最多等待3秒,上锁以后10秒后过期自动解锁 所以等了3秒都等不到,就放弃了

总结

上面介绍了分布式重入锁的相关知识,证明了Redisson工具能实现了可重入锁的功能。其实Redisson工具包中还包含了读写锁(ReadWriteLock)和 红锁(RedLock)等相关功能,我们下篇文章再详细研究。

更多编程相关知识,请访问:编程入门!!

以上就是什么是可重入锁?详解redis实现分布式重入锁的方法的详细内容,转载自php中文网

点赞(918) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部