一文搞懂:共享锁、排它锁、乐观锁、悲观锁——它们到底是什么关系?
写在前面在学习Java并发编程和数据库事务的过程中“锁”这个概念几乎无处不在。共享锁、排它锁、互斥锁、读锁、写锁、乐观锁、悲观锁……光是听到这些名字就已经让人一头雾水了。更让人困惑的是这些锁的名称还经常互相混用——读锁就是共享锁吗互斥锁就是排它锁吗乐观锁和悲观锁跟前面那些锁是什么关系它们是在Java中存在的还是在数据库中存在的还是两者都存在要搞清楚这些问题关键不是死记硬背锁的名字而是理解一个东西这些锁是从不同角度来分类的不要把不同分类标准的锁混在一起比较那必然乱套。1️⃣ 锁的分类维度三个角度看懂所有锁“锁”这个字写在纸上但从不同的角度看过去看到的是完全不同的东西。所以你的困惑“共享锁是不是就是乐观锁”之类的——本质就是把不同角度的锁混在一起做比较了。锁有三层分类逻辑彼此是“正交”的互不重叠三层分类的逻辑关系如下三个维度之间的关系是策略层决定用悲观还是乐观的态度 →能力层决定执行时提供共享还是排它的权限 →落地层决定在JVM内存还是数据库里以什么粒度落地执行。接下来针对你关心的“概念混淆”问题逐一从这三个维度拆解。2️⃣ 按行为模式分类乐观锁 vs 悲观锁策略层这两种锁并不是某种具体的锁而是处理并发问题的两种“战略方针”。从场景出发做人的态度在现实生活中如果我们要去一个需要排队的窗口办业务比如银行柜台我们的应对策略会因为对别人的不信任程度而有所变化悲观的人悲观锁——“怎么会有人没底线呢”先把窗口占住然后才办理业务。办理的过程中把所有后面来的人全挡在外面。这样办起来特别稳但其他人全部进不来。乐观的人乐观锁不占窗口只是在电脑上提交申请时附上“我的身份证号是xxx、我是第xx号”的凭证。如果在这个瞬间系统发现你的名额被人抢先占用了版本号变了就退回你的申请告诉你“冲突了回去重办”。办得顺畅时速度很快但需要不断核对核对的过程中可能跟别人互飙速度。明白了这个态度选择再接着看它们的技术实现规则悲观锁Pessimistic Lock【态度】我在做操作的时候一定会有人来抢所以我先下手为强先把锁占住在Java中的实现// synchronized一行代码直接锁就是最纯粹的悲观锁 public synchronized void deductStock() { ... } // ReentrantLock手动lock/unlock也是悲观锁 Lock lock new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }Java中的synchronized和ReentrantLock就是悲观锁。它在JVM层面锁住了某个对象或代码块一旦有线程进入其他线程必须阻塞等待。在MySQL中的实现BEGIN; -- 手动加排他锁直接锁住这一行其他事务别想动 SELECT * FROM product WHERE id 1 FOR UPDATE; UPDATE product SET stock stock - 1 WHERE id 1; COMMIT;MySQL的SELECT ... FOR UPDATE就是数据库层面的悲观锁。【到底好用在哪 疼在哪】优点非常可靠、强一致能绝对保证并发修改不会相互覆盖。缺点性能差锁等待时间长容易引发死锁不适合高并发场景。适用场景写多读少写操作量大、数据一致性要求极高如金融转账、库存扣减的场景。乐观锁Optimistic Lock【态度】我觉得不会有人跟我抢我先干我的活儿最后确认一下没被人改过就行。在Java中的实现// AtomicInteger 基于 CAS整个过程不阻塞其他线程 AtomicInteger stock new AtomicInteger(100); stock.compareAndSet(100, 99); // CAS操作如果当前值是100就设为99否则重试Java中的AtomicInteger、AtomicLong等原子类本质上是基于CASCompare And Swap比较并交换实现的乐观锁。它利用CPU硬件指令保证比较和交换的原子性实现无锁编程。在MySQL中的实现-- 给表加一个版本号字段 UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND version 5; -- 如果影响行数为0说明version已被改过需要重试MySQL本身并不提供乐观锁语法乐观锁是在应用层通过版本号或时间戳手动实现的并发控制策略。也可以用框架实现JPA中的Version注解。在Spring Data JPA / Hibernate中的实现Entity public class Product { Version private Integer version; // 加了这个注解自动实现乐观锁 }Hibernate会在更新时自动检查版本号如果版本不一致就抛OptimisticLockException。【到底好用在哪 疼在哪】优点无锁或轻量锁性能高适合读多写少的场景。缺点冲突率高时会频繁重试消耗CPUABA问题值从A变B又变回ACAS误以为没变过需要额外处理。适用场景读多写少修改频率低、数据冲突概率小如用户资料更新、状态机流转。3️⃣ 按权限操作分类共享锁 vs 排它锁能力层这种分类是告诉你这个锁的能力有多强允许多人一起读还是只能一个人独享共享锁Shared Lock—— 读锁规则多个事务/线程可以同时持有共享锁大家都只读不写互不干扰。但一旦有人加了共享锁就禁止任何排他锁加进来。S锁与S锁兼容S锁与X锁互斥。兼容矩阵如下在Java中的实现Java中的共享锁代表是ReentrantReadWriteLock的读锁部分ReadWriteLock rwLock new ReentrantReadWriteLock(); Lock readLock rwLock.readLock(); // 共享锁 Lock writeLock rwLock.writeLock(); // 排它锁 // 读锁多个线程可以同时获取 readLock.lock(); try { // 读取共享数据这里实际上是共享锁允许多个读线程并发 } finally { readLock.unlock(); }读写锁的核心设计理念正是基于共享/排它锁的兼容规则——“读读共享读写互斥写写互斥”。在MySQL中的实现-- 显式加共享锁MySQL 8.0 SELECT * FROM product WHERE id 1 FOR SHARE; -- MySQL 8.0之前的写法 SELECT * FROM product WHERE id 1 LOCK IN SHARE MODE;加了共享锁后其他事务可以继续加共享锁读但不能加排他锁写保证了读操作的一致性。排它锁Exclusive Lock—— 写锁 / 互斥锁规则排他锁是独占性质的一个事务/线程持有X锁后其他所有事务/线程都不能加任何锁S或X也无法进行写入操作。简单来说就是“生人勿近老子全占”。在Java中的实现ReentrantReadWriteLock的写锁就是典型的排它锁此外这个角色几乎也由ReentrantLock和synchronized来扮演。可以说排它锁是所有Java悲观锁的内核属性。rwLock.writeLock().lock(); try { // 写操作此时其他线程既不能读也不能写 } finally { rwLock.writeLock().unlock(); }但注意Java中的排它锁不一定非得配共享锁配套出现在很多场景下如synchronized修饰的普通方法我们直接用排他独占模式而不提供“读共享”的路径。在MySQL中的实现-- 手动排他锁 SELECT * FROM product WHERE id 1 FOR UPDATE; -- DML语句会自动加排他锁 UPDATE product SET stock stock - 1 WHERE id 1; UPDATE/DELETE/INSERT语句会自动给涉及的行加X锁[reference:11]。 ### 互斥锁Mutex又是什么 你提到的**互斥锁**在Java和操作系统领域里可以理解为**排它锁的别名**。它的核心就是“独占”——同一时刻只允许一个线程持有该锁。在Java中synchronized和ReentrantLock就是互斥锁的经典实现[reference:12]。 在MySQL中排它锁就是互斥锁的体现——一个事务加了X锁之后其他事务无法加任何锁。所以简单粗暴地记住**互斥锁 排它锁的别称**。不是一个全新的东西。只是在并发编程中“互斥”这个词更强调这种锁的独占行为本身而排它锁更体现的是它的“能力”。 ## 4️⃣ 按实现粒度分类表锁、行锁、间隙锁落地层 这一层决定的是“在哪个范围内加锁”锁的颗粒度越大并发性越低。 ### 4.1 MySQL InnoDB 的锁粒度体系 InnoDB存储引擎支持多层次锁粒度从粗到细依次为 mermaid flowchart LR subgraph Granularity [锁粒度] direction LR A[全局锁br整库只读] -- B[表锁br锁定整张表] B -- C[行锁br锁定单行记录] C -- D[间隙锁br锁定索引间隙] D -- E[临键锁br记录锁间隙锁] end4.2 Java 层面的锁粒度JVM层Java的锁主要作用于JVM内存中的对象对象锁锁住一个具体的对象实例如synchronized(this)类锁锁住整个类的Class对象如synchronized(Xxx.class)所有实例共享一把锁代码块锁锁住一段指定的代码区域粒度比对象锁更细可以精细控制和数据库不同Java锁没有“行锁”或“页锁”的概念因为Java锁锁的是代码和内存中的对象不是数据表的记录。数据库锁锁的是持久化的数据行而Java锁锁的是运行时内存中的对象。5️⃣ Java中的锁 vs 数据库中的锁这也是你的疑惑——“这些锁到底存在在哪里”答案是他们分属不同的地盘但思想相通。一句话总结Java里的锁管的是代码逻辑数据库里的锁管的是数据本身。做微服务的时候你的服务可能是分布式集群Java自带的锁在这个场景下越狱不出去需要依赖数据库锁或者分布式锁来保证跨节点的互斥。6️⃣ 全链路横向对比一张深度对比表看懂所有锁每个锁都有自己专属的“历史使命”。不同维度的锁之间不但不冲突反而需要互相搭配。7️⃣ 实战场景不同场景该用什么锁在写代码前先冷静地判断两件事1这是Java层锁、数据库锁还是分布式锁 2结合数据冲突概率决定用乐观锁还是悲观锁8️⃣ 常见面试题与避坑指南Q1共享锁就是乐观锁吗不是。共享锁读锁是一种锁的能力描述的是“允许多个事务同时读”。乐观锁是一种锁的策略描述的是“假设不会冲突最后用版本号检验”。两者完全不同。Q2互斥锁就是排它锁吗可以近似理解为是同一个东西。互斥锁的核心就是“独占”——和排它锁的能力完全一致。在Java中互斥锁一般指synchronized和ReentrantLock在数据库中互斥锁就是排它锁。Q3Java中的synchronized是悲观锁还是乐观锁悲观锁。它的态度是“我先锁住不让别人进来”这是典型的悲观策略。Q4SELECT ... FOR UPDATE加了什么锁排它锁写锁。FOR UPDATE会让InnoDB给命中的行或间隙加排它锁防止其他事务读或写。Q5什么是ABA问题怎么解决CAS乐观锁中值从A变成B又变回ACAS看到还是A就认为没变过但实际上已变过。用AtomicStampedReference或数据库版本号version字段解决。避坑指南用Redis分布式锁时记得设置超时时间防止死锁。高并发下尽量用乐观锁少用悲观锁。悲观锁的行锁会大幅降低系统吞吐量。MySQL意向锁是自动加的不需要也不允许手动干涉。InnoDB行锁是基于索引的没命中索引会升级为表锁。在一个高并发的秒杀系统中有人主张用数据库乐观锁重试机制来防止超卖有人坚决主张用Redis分布式锁数据库悲观锁FOR UPDATE。请分析这两种方案的核心矛盾在哪里若让你设计你会怎么组合不同层次的锁来达到“既要极速响应又要数据0误差”的双重目标欢迎在评论区分享你的设计方案和踩坑经验