当前位置:

首页 > 编程开发 > Java与MySQL锁相关的知识点有哪些?

Java与MySQL锁相关的知识点有哪些?

锁的定义在计算机程序中锁用于独占资源,获取到锁才可以操作对应的资源。锁的实现锁在计算机底层的实现,依赖于CPU提供的CAS指令(compareandswsp),对于一个内存地址,会比较原值以及尝试去修改的值,通过值是否修改成功,来表示是否强占到了这个锁。JVM中的锁jvm中,有2个常用的锁synchronizedsynchronized是java提供的关键字锁,可以锁对象,类,方法。在JDK1.6以后,对synchronized进行了优化,增加了偏向锁和轻量锁模式,现在synchronized锁的运行逻辑

本文目录

      锁的定义

      在计算机程序中锁用于独占资源,获取到锁才可以操作对应的资源。

      锁的实现

      锁在计算机底层的实现,依赖于CPU提供的CAS指令(compare and swsp),对于一个内存地址,会比较原值以及尝试去修改的值,通过值是否修改成功,来表示是否强占到了这个锁。

      JVM中的锁

      jvm中,有2个常用的锁

      synchronized

      synchronized是java提供的关键字锁,可以锁对象,类,方法。
      在JDK1.6以后,对synchronized进行了优化,增加了偏向锁和轻量锁模式,现在synchronized锁的运行逻辑如下:

      • 在初始加锁时,会增加偏向锁,即“偏向上一次获取该锁的线程”,在偏向锁下,会直接CAS获取该锁。该模式大大提高了单线程反复获取同一个锁的吞吐情况,在Java官方看来,大部分锁的争抢都发生在同个线程上。

      • 如果偏向锁CAS获取失败,说明当前线程与偏向锁偏向的线程不同,偏向锁就会升级成轻量锁,轻量锁的特点就是通过自旋CAS去获取锁。

      • 如果自旋获取失败,那么锁就会升级成重量锁,所有等待锁的线程将被JVM挂起,在锁释放后,再由JVM统一通知唤醒,再去尝试CAS锁,如果失败,继续挂起。

      很显然,偏向锁设计的目的是“在Java官方看来,对同一个锁的争抢大部分都发生在同个线程上”。
      轻量锁设计的目的是“在短期内,锁的争抢通过自旋CAS就可以获取到,短时间内的CPU自旋消耗小于线程挂起再唤醒的消耗”。
      重量锁就是最初优化前的synchronized的逻辑了。

      ReentrantLock

      说到ReentrantLock,就不得不说到JUC里的AQS了。
      AQS全称AbstractQueueSynchronizer,几乎JUC里所有的工具类,都依赖AQS实现。
      AQS在java里,是一个抽象类,但是本质上是一种思路在java中的实现而已。
      AQS的实现逻辑如下:

      • 构造一个队列

      • 队列中维护需要等待锁的线程

      • 头结点永远是持有锁(或持有资源)的节点,等待的节点在头结点之后依次连接。

      • 头结点释放锁后,会按照顺序去唤醒那些等待的节点,然后那些节点会再次去尝试获取锁。

      在synchronized锁优化以后,AQS的本质与synchronized并没有太大不同,两者的性能也并没有太大差距了,所以AQS现在的特点是:

      • 是在java api层面实现的锁,所以可以实现各种并发工具类,操作也更加灵活

      • 因为提供了超时时间等机制,操作灵活,所以不易死锁。(相同的,如果发生死锁,将更难排查,因为jstack里将不会有deadlock标识)。

      • 可以实现公平锁,而synchronized必定是非公平锁。

      • 因为是JavaApi层实现的锁,所以可以响应中断。

      到这里你会发现,其实ReentrantLock可以说是synchronized在JavaApi层的实现。

      Mysql 锁

      共享锁(S) 与排它锁(X)

      作用范围

      这两种锁都包括行级锁和表级锁。
      获取共享锁时,如果该数据被其他事务的排它锁锁住,则无法获取,需要等待排它锁释放。

      意向锁

      作用范围

      意向锁为表锁,在获取表锁之前,一定会检查意向锁。

      意图锁定协议如下:

      在事务获得表中某行的共享锁之前,它必须首先获得表上的 IS 锁或更强的锁。

      在事务获得表中行的排他锁之前,它必须首先获得表的 IX 锁。

      在获取任意表锁的共享锁或排它锁之前,一定会检查该表上的共享锁。

      表锁以及意向锁的互斥规则如下:
      X IX S IS
      X Conflict Conflict Conflict Conflict
      IX Conflict Compatible Conflict Compatible
      S Conflict Conflict Compatible Compatible
      IS Conflict Compatible Compatible Compatible

      意向锁的作用在于:在获取表锁时,可以通过意向锁来快速判断能否获取。

      因为获取行级锁时,会先获取对应的意向锁,这样另外的事务在获取表锁时就可以通过意向锁快速的判断,而不需要每行去扫描。

      特别注意的是,意向锁是可以叠加的,即会存在多个,如T1事务获取了意向锁IX1和行级锁X1,T2事务依旧可以获取意向锁IX2和行级锁X2,所以仅在获取表级锁之前,才会检查意向锁。

      记录锁

      记录锁生效在索引上,用以在SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE时保护该行数据不被其他事务更改。

      记录锁在没有索引时依旧会生效,因为innodb会为每张表创建一个隐藏的索引。

      记录锁是最基本的行锁。

      间隙锁

      间隙锁生效在索引上,用于锁定索引值后的行,防止插入,在select from table where index=? for update时会生效,例如index=1,则会锁住index=1索引节点相关的行,防止其他事务插入数据。

      但是并不会防止update语句,哪怕update的数据不存在。

      Next-Key Locks

      这个锁是记录锁和间隙锁的组合,简而言之在select from table where index=? for update时,既会有间隙锁防止insert,也会有记录锁在index上防止这一条数据的update和delete。这个Next-key只是对这两种锁的一种概括,因为这两种锁在select for update时通常会一起出现。

      Insert Intention Locks

      插入意向锁,和意向锁类似。不过是特殊的间隙锁,并不发生在select for update,而是在同时发生insert时产生,例如在两个事务同时insert索引区间为[4,7]时,同时获得该区间的意向锁,此时事务不会阻塞,例如A:insert-5,B:insert-7,此时不会阻塞两个事务。

      插入意向锁是一个特殊的间隙锁,是为了防止正常间隙锁锁区间的情况下,insert频繁阻塞而设计的,例如A:insert-5,B:insert-7,如果没有插入意向锁,那么5和7都要去尝试获取间隙锁,此时第二个事务就会被阻塞,但是通过插入意向锁,第二个事务就不会被阻塞,只有到插入的行确实冲突,才会被阻塞。

      AUTO-INC Locks

      自增锁,这个锁很明显是表级insert锁,为了保证自增主键的表的主键保持原子自增。

      对于锁这个东西,大家应该多去理解各种锁设计运行的原理和模型,这样在加深理解后,在使用起来才会更加深入和透彻。

      常见锁使用的场景和用法

      double check

      众所周知,mysql的事务对防止重复插入并没有什么卵用,唯一索引又存在很多缺点,业务上最好不要使用,所以一般来说防止重复插入的通用做法就是使用分布式锁,这就有一种比较常用的写法。

      final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId());
      if (weekendNoticeReadCountDO == null) {
          final String lockKey = RedisConstant.LOCK_WEEKEND_READ_COUNT_INSERT + ":" + noticeRequestDTO.getNoticeId();
          ClusterLock lock = clusterLockFactory.getClusterLockRedis(
              RedisConstant.REDIS_KEY_PREFIX,
              lockKey
          );
          if (lock.acquire(RedisConstant.REDIS_LOCK_DEFAULT_TIMEOUT)) {
              //double check
              final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId());
              if (weekendNoticeReadCountDO == null) {
                  try {
                      lock.execute(() -> {
                          WeekendNoticeReadCountDO readCountDO = new WeekendNoticeReadCountDO();
                          readCountDO.setNoticeId(noticeRequestDTO.getNoticeId());
                          readCountDO.setReadCount(1L);
                          readCountDO.setCreateTime(new Date());
                          readCountDO.setUpdateTime(new Date());
                          weekendNoticeReadRepositoryService.insert(readCountDO);
                          return true;
                      });
                  } catch (ApiException err) {
                      throw err;
                  } catch (Exception e) {
                      log.error("插入", e);
                      throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服务端出错");
                  }
              } else {
                  weekendNoticeReadRepositoryService.noticeCountAdd(weekendNoticeReadCountDO);
              }
          } else {
              log.warn("redis锁获取超时,key:{}", lockKey);
              throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服务器繁忙,请稍后重试");
          }
      }

      在获取到锁之后,可能是经过等待才获取到的锁,此时上一个释放锁的线程可能已经插入了数据了,所以在锁内部,依旧要再次校验一下数据是否存在。
      这种写法适合大多数需要唯一性的写场景。

      避免死锁

      如何避免死锁?最简单有效的方法就是:**不要在锁里再去获取锁,简而言之就是锁最好单独使用,不要套娃。
      也要注意一些隐性锁,比如数据库。
      事务A:

      • 插入[5,7],插入意向锁。

      • select for update更新[100,150],间隙锁。
        事务B:

      • select for update更新[90,120],间隙锁。

      • 插入[4,6],插入意向锁。

      此时在并发场景下,就可能会出现A持有了[5,7]的间隙锁,在等待事务B[90,120]的间隙锁,事务B也一样,就死锁了。
      **

      顺带谈谈并发场景下常见的问题

      读写混乱

      在写业务代码,定义一些工具类或者缓存类的时候,很容易疏忽而发生类似的问题。
      比如构建一个static缓存,没有使用ConcurrentHashMap中的putIfAbsent等方法,也没有加锁去构建,导致上面的线程刚put了,下面的线程就删掉了,或者重复构建2次缓存。

      Redis或者一些并发操作释放锁或者资源,没有检查是否是当前线程持有

      这点在Redis锁的示例代码也讲到了。
      线程A获取到锁,此时B,C在等待,然后A执行时间过长,导致锁超时被自动释放了,此时B获取到了锁,在快乐的执行,然后A执行完了之后,释放锁时没有判断是否还是自己持有,导致B持有的锁被删除了,此时C又获取到了锁,BC同时在执行。

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java MySQL
      相关文章 更多
      navicat导入数据库步骤详解及常见问题处理教程
      navicat导入数据库步骤详解及常见问题处理教程

      详解Navicat导入数据库的标准步骤,涵盖SQL脚本执行与CSV数据导入两种核心场景。针对导入过程中常见的字符集乱码、主键冲突及外键约束错误,提供具体的设置方法与修复建议,确保数据迁移准确无误。

      SQL主键约束和唯一约束有什么区别
      SQL主键约束和唯一约束有什么区别

      深入理解SQL PRIMARY KEY与UNIQUE约束的区别。文章从行标识与重复限制的目标出发,详解主键的非空特性、外键引用角色以及多数据库对NULL值的处理差异。结合users表实战示例,指导开发者如何根据字段稳定性与业务语义合理选择约束类型,避免设计误区。

      SQL约束有哪些类型及作用
      SQL约束有哪些类型及作用

      刚开始设计关系型数据库?了解PRIMARY KEY、FOREIGN KEY、UNIQUE、NOT NULL、CHECK、DEFAULT等约束的作用、语法和排错方法。本文以PostgreSQL语法为主,结合用户订单场景,清晰展示如何防止脏数据并处理常见约束冲突。

      SQL游标常见错误排查:死锁与资源未释放问题
      SQL游标常见错误排查:死锁与资源未释放问题

      深入解析SQL游标导致的死锁与资源未释放问题。通过PostgreSQL与MySQL语法对比,展示如何识别长事务持锁、分析锁等待链,并提供短事务提交、异常清理及批量处理的最佳实践,确保数据库连接与锁资源及时释放。

      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      SQL中怎么创建和删除约束
      SQL中怎么创建和删除约束

      后端开发者在维护数据库时,常需对现有表添加主键、唯一或外键约束。若直接执行ADD CONSTRAINT,往往因存量数据不合规导致报错。本文详解操作前的数据清洗步骤、标准SQL语法及MySQL等特殊写法,帮助你在生产环境安全完成结构变更。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      SQL分组统计查询:GROUP BY与HAVING子句用法示例
      SQL分组统计查询:GROUP BY与HAVING子句用法示例

      针对 SQL 初学者的分组统计实战指南。深入解析 GROUP BY 分组机制、WHERE 与 HAVING 的执行时机差异,以及 COUNT 函数对 NULL 值的处理逻辑。通过具体代码示例演示如何正确筛选聚合结果,并排查因 JOIN 连接导致的统计数据异常,提升报表准确性。

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。