热门标签 | 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


推荐阅读
  • 驱动程序的基本结构1、Windows驱动程序中重要的数据结构1.1、驱动对象(DRIVER_OBJECT)每个驱动程序会有唯一的驱动对象与之对应,并且这个驱动对象是在驱 ... [详细]
  • Java高级工程师学习路径及面试准备指南
    本文基于一位朋友的PDF面试经验整理,涵盖了Java高级工程师所需掌握的核心知识点,包括数据结构与算法、计算机网络、数据库、操作系统等多个方面,并提供了详细的参考资料和学习建议。 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 来自FallDream的博客,未经允许,请勿转载,谢谢。一天一套noi简直了.昨天勉强做完了noi2011今天教练又丢出来一套noi ... [详细]
  • java datarow_DataSet  DataTable DataRow 深入浅出
    本篇文章适合有一定的基础的人去查看,最好学习过一定net编程基础在来查看此文章。1.概念DataSet是ADO.NET的中心概念。可以把DataSet当成内存中的数据 ... [详细]
  • SPFA算法详解与应用
    当图中包含负权边时,传统的最短路径算法如Dijkstra不再适用,而Bellman-Ford算法虽然能解决问题,但其时间复杂度过高。SPFA算法作为一种改进的Bellman-Ford算法,能够在多数情况下提供更高效的解决方案。本文将详细介绍SPFA算法的原理、实现步骤及其应用场景。 ... [详细]
  • 春季职场跃迁指南:如何高效利用金三银四跳槽季
    随着每年的‘金三银四’跳槽高峰期的到来,许多职场人士都开始考虑是否应该寻找新的职业机会。本文将探讨如何制定有效的职业规划、撰写吸引人的简历以及掌握面试技巧,助您在这关键时期成功实现职场跃迁。 ... [详细]
  • 深入理解Java SE 8新特性:Lambda表达式与函数式编程
    本文作为‘Java SE 8新特性概览’系列的一部分,将详细探讨Lambda表达式。通过多种示例,我们将展示Lambda表达式的不同应用场景,并解释编译器如何处理这些表达式。 ... [详细]
  • JUC并发编程——线程的基本方法使用
    目录一、线程名称设置和获取二、线程的sleep()三、线程的interrupt四、join()五、yield()六、wait(),notify(),notifyAll( ... [详细]
  • 大华股份2013届校园招聘软件算法类试题D卷
    一、填空题(共17题,每题3分,总共51分)1.设有inta5,*b,**c,执行语句c&b,b&a后,**c的值为________答:5 ... [详细]
  • 本文详细介绍了 Java 网站开发的相关资源和步骤,包括常用网站、开发环境和框架选择。 ... [详细]
  • 本文总结了Java初学者需要掌握的六大核心知识点,帮助你更好地理解和应用Java编程。无论你是刚刚入门还是希望巩固基础,这些知识点都是必不可少的。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了重温Linux内核:互斥和同步相关的知识,希望对你有一定的参考价值。文章目录 ... [详细]
  • 该楼层疑似违规已被系统折叠隐藏此楼查看此楼错误72error:ErroropeningoutputfileC:Users林鑫辰AppDataLocalTemptmpxft_0000 ... [详细]
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社区 版权所有