热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

锁分布式锁工具

锁概述在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。锁相关概念锁开销:完成一个锁可能额外耗费的资源,比如一个周期所

锁 - 分布式锁工具

锁概述

在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。

锁相关概念

  • 锁开销:完成一个锁可能额外耗费的资源,比如一个周期所需要的时间,内存空间。
  • 锁竞争:一个线程或进程,要获取另一个线程或进程所持有的锁,边会发生锁竞争。锁粒度越小,竞争的可能越小。
  • 死锁:多个线程争夺资源互相等待资源释放导致阻塞;由于无限期阻塞,程序不能正常终止。

分类

  • 乐观锁、悲观锁:是否锁定同步资源。
    • 乐观锁:认为其他线程对数据访问时 不会 修改数据,实际未加锁,更新数据时判断是否被其他线程更新了(读时不加锁,写时加锁)。
      • 适合多读的场景,因为读操作没有加锁。
      • 实现原理:CAS (compare-and-swap) ,无锁算法,原子操作比较更新。
      • 使用:
        • Java 中的 CAS 锁(AtomicXxx)通过 JNI 调用 CPU 中的 cmpxchg 汇编指令实现
        • 数据库表增加 version 字段,更新时判断 version 未改变。
      • 缺陷:
        • ABA 问题:数据发生类似变化(A -> B -> A),会认为数据没有改变。
          JDK 1.5 引入 AtomicStampedReference 增加标志位(1A -> 2B -> 3A)
        • 自旋问题:CAS 无法获取到锁会在超时时间内循环获取,造成 CPU 资源浪费
    • 悲观锁:认为其他线程对数据访问时 一定会 修改数据,访问数据时加锁同步处理(一开始加锁无论读写)。
      • 适合多写的场景,独占数据的读写权限,确保数据的读取和更新都是准确的。
  • 读写锁
    • 读锁:共享锁,可支持多线程并发读。
    • 写锁:独享锁,读写、写写互斥。
    • 示例:ReentrantReadWriteLock
  • 可重入锁、不可重入锁
    • 可重入锁(递归锁):一个线程在已加锁范围内代码中再次进行加锁能够获取到锁
      • synchronized 、 ReentrantLock
    • 不可重入锁:一个线程对在已加锁范围内代码中再次进行加锁操作,由于第二次加锁时需要等待上次锁释放才可以加锁造成锁的互相等待
  • 公平锁、非公平锁
    • 公平锁:多个线程按照申请锁的顺序来获取锁,依赖 AQS 队列,线程直接进入队列中排队,第一个线程才能获取到锁
    • 非公平锁:多个线程加锁时尝试直接获取锁,获取不到进入队列,可能出现后申请锁的线程先获取到锁
      • 优点:可以减少唤起线程的开销,整体吞吐效率高
      • 缺点:处于等待队列中的线程可能饿死
      • synchronized
    • 示例:ReentrantLock 默认为非公平锁,构造方法可指定为公平锁 new ReentrantLock(true);
  • 偏向锁、轻量锁、重量锁:synchronized 的三种锁状态。
    • 偏向锁:锁标志位 101,在对象头(Mark Word)和栈帧中锁记录(Lock Record)里存储线程ID,通过 对比 Mark Word 避免执行 CAS
      • JDK 6 引入,JDK 15 标记废弃,可通过 JVM 参数(-XX:+UseBiasedLocking)手动启用
    • 轻量锁:锁标志位 000,偏向锁时出现竞争升级为轻量锁,未获取到锁的线程自旋获取,通过 CAS + 自旋 避免线程阻塞唤醒
    • 重量锁:锁标志位 010,轻量锁自旋超过一定此处升级为重量锁,未获取到锁的线程休眠
  • 分段锁、自旋锁:锁设计,非特定的锁。
    • 分段锁:将要锁定的数据拆分成段后对所需数据段加锁,减少锁定范围
      • ConcurrentHashMap 在 JDK 8 之前使用 Segment (继承 ReentrantLock)对桶数组分割分段加锁
    • 自旋锁:试探获取资源,未获取到采取自旋循环 where(true) 再次试探获取,不阻塞线程
      • 轻量锁通过 CAS + 自旋 实现
      • 优点:减少上下文切换
      • 缺点:占用 CPU

相关阅读:

  • Java中的锁 - 沈三白
  • 听说你知道什么是锁 --JAVA - 罗小扇

自定义锁工具

1 :Redis 分布式锁(简单实现)

使用 ThreadLocal 保存锁对应的唯一标识
加锁:使用 STRING 保存锁定标识, "SET key value PX NX" 确保一个 key 只能加锁一次
解锁:Lua 脚本判断是自己加的锁进行释放

  • 工具类

    RedisSimpleLockUtil.java
    // 使用 ThreadLocal 保存锁对应的唯一标识
    private static final ThreadLocal LOCK_FLAG = ThreadLocal.withInitial(() ->
            UUID.randomUUID().toString().replace("-", "").toLowerCase()
    );
    
    // 尝试加锁
    private boolean tryLock(String key, long ttl) {
        try {
            String val = LOCK_FLAG.get();
            Boolean lockRes = redisTemplate.opsForValue()
                    .setIfAbsent(key, val, ttl, TimeUnit.MILLISECONDS);
            log.debug("tryLock, key={}, val={}, lockRes={}", key, val, lockRes);
            return Boolean.TRUE.equals(lockRes);
        } catch (Exception e) {
            log.error("tryLock occurred an exception", e);
        }
    
        return false;
    }
    
    // 解锁
    public boolean unlock(String key) {
        boolean succeed = false;
        try {
            List keys = Collections.singletonList(key);
            Object[] args = {LOCK_FLAG.get()};
            Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args);
            log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes);
            succeed = Optional.ofNullable(unlockRes).filter(res -> res > 0).isPresent();
        } catch (Exception e) {
            log.error("unlock occurred an exception", e);
        } finally {
            if (succeed) {
                LOCK_FLAG.remove();
            }
        }
    
        return succeed;
    }
    
  • Lua 脚本

    解锁: redis_unlock_simple.lua
    local lock_key = KEYS[1];
    local lock_flag = ARGV[1];
    
    --- 判断锁定的唯一标识与参数一致删除锁
    --- 返回值:1=解锁成功(删除成功),0=锁已失效或删除失败,-1=非自己的锁不支持解锁
    local val = redis.call("GET", lock_key);
    if (not val) then
        return 0;
    elseif (val == lock_flag) then
        return redis.call("DEL", lock_key);
    else
        return -1;
    end
    
  • 缺陷

    • 只能单次加锁(唯一标识通过 ThreadLocal 存储,解锁时会清理 ThreadLocal,多次加解锁会导致与预期不符)
    • 不可重入
  • 参考:https://github.com/realpdai/tech-pdai-spring-demos/blob/main/264-springboot-demo-redis-jedis-distribute-lock/src/main/java/tech/pdai/springboot/redis/jedis/lock/lock/RedisDistributedLock.java

2 :Redis 分布式锁

使用 ThreadLocal 保存 锁key 与 相应的唯一标识
加锁:使用 HASH 保存锁标识与加锁次数
解锁:Lua 脚本判断是自己加的锁进行释放
功能:可重入(Redis HASH)、支持对不同 key 进行加解锁(ThreadLocal>)

  • 工具类

    RedisLockUtil.java
    // 使用 ThreadLocal 保存 锁key 与 唯一标识
    private static final ThreadLocal> LOCK_FLAG =
            ThreadLocal.withInitial(HashMap::new);
    // 尝试加锁
    private long tryLock(String key, long ttl) {
        String uniqueFlag = LOCK_FLAG.get().get(key);
        if (uniqueFlag == null) {
            uniqueFlag = UUID.randomUUID().toString().replace("-", "");
            LOCK_FLAG.get().put(key, uniqueFlag);
        }
    
        try {
            List keys = Collections.singletonList(key);
            Object[] args = {uniqueFlag, ttl};
            Long lockRes = redisTemplate.execute(LOCK_SCRIPT, keys, args);
            log.debug("tryLock, lock_flag={}, key={}, args={}, lockRes={}",
                    LOCK_FLAG.get(), key, args, lockRes);
            return lockRes != null ? lockRes : 0L;
        } catch (Exception e) {
            log.error("tryLock occurred an exception", e);
        }
    
        return 0L;
    }
    
    // 尝试解锁
    public long tryUnlock(String key) {
        String uniqueFlag = LOCK_FLAG.get().get(key);
        if (uniqueFlag == null) {
            return 0L;
        }
    
        long lockNum = -1L;
        try {
            List keys = Collections.singletonList(key);
            Object[] args = {uniqueFlag};
            Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args);
            log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes);
            lockNum = unlockRes != null ? unlockRes : 0L;
        } catch (Exception e) {
            log.error("release lock occurred an exception", e);
        } finally {
            if (lockNum == 0L) {
                LOCK_FLAG.get().remove(key);
                if (LOCK_FLAG.get().isEmpty()) {
                    LOCK_FLAG.remove();
                }
            }
        }
    
        return lockNum;
    }
    
  • Lua 脚本

    加锁: redis_lock.lua
      ```lua
      local lock_key = KEYS[1];
      local lock_flag = ARGV[1];
      --- 锁定时长,单位:毫秒
      local lock_ttl = tonumber(ARGV[2]);
    
      --- HASH 支持可重入
      --- lock_flag 保存加锁唯一标识
      --- lock_num 保存加锁次数
      local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num");
      local h_flag = info[1];
      local h_num = tonumber(info[2]);
      if (h_num == nil or h_num <0) then
          h_num = 0;
      end
    
      --- 返回加锁次数,未加锁成功返回 -1
      if (not h_flag or h_flag == lock_flag) then
          local res_num = h_num + 1;
          redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num);
          redis.call("PEXPIRE", lock_key, lock_ttl);
          return res_num;
      else
          return -1;
      end
      ```
    
    解锁: redis_lock.lua
      ```lua
      local lock_key = KEYS[1];
      local lock_flag = ARGV[1];
    
      --- HASH 支持可重入
      --- lock_flag 保存加锁唯一标识
      --- lock_num 保存加锁次数
      local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num");
      local h_flag = info[1];
      local h_num = tonumber(info[2]);
      if (h_num == nil) then
          h_num = 0;
      end
    
      --- 返回剩余加锁次数,未被加锁或解锁完返回 0,非自己加锁返回 -1
      if (not h_flag) then
          return 0;
      elseif (h_flag == lock_flag) then
          if (h_num <= 0) then
              redis.call("DEL", lock_key);
              return 0;
          else
              local res_num = h_num - 1;
              redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num);
              return res_num;
          end
      else
          return -1;
      end
      ```
    

其他

demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-lock


推荐阅读
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
author-avatar
耗子很傻爱钻洞
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有