Skip to content

锁的这些概念——synchronizedReentrantLock、悲观锁、乐观锁、分布式锁——平时写代码零零散散都接触过,但一直没有系统、全面地梳理过。这篇把自己散落的知识点整理成一张全景图:锁有哪些种类、每一类是怎么实现的、什么时候该用哪个。


一、锁解决什么问题

并发下,多个线程(或进程)同时访问一份共享资源,如果不加控制,就会数据错乱——典型的就是"超卖":库存只剩 1,两个请求同时读到 1,都下单,卖出去 2 件。

锁 = 保证"同一时刻只有一个执行单元能访问共享资源"的机制。 谁拿到锁谁访问,别人等着。

二、按作用范围分:本地锁 vs 分布式锁

这是最顶层、也最容易搞混的一个划分。

类型作用范围典型实现管不管别的机器
本地锁单个 JVM 进程内synchronizedReentrantLock不管,别的机器看不见
分布式锁跨 JVM / 跨机器Redis、ZooKeeper、数据库管,全局互斥

一句话判断:你的服务是单机部署还是多机部署?单机用本地锁就够;多台服务器抢同一份资源(扣库存、抢优惠券),本地锁管不到别的机器,必须上分布式锁。

三、本地锁怎么实现

1. synchronized(JVM 内置)

synchronized 是 Java 关键字,锁的是对象。JDK 1.6 之后做了大量优化,有个经典的"锁升级"过程:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 偏向锁:只有一个线程反复访问时,对象头记录这个线程 ID,进来时比一下 ID 就行,几乎零开销。
  • 轻量级锁:出现第二个线程竞争时,用 CAS(Compare And Swap)自旋尝试抢,线程在用户态空转,不阻塞。
  • 重量级锁:自旋失败、竞争激烈,升级成操作系统层面的互斥量(mutex),线程阻塞、挂起,交给操作系统调度。

补充:偏向锁因为维护成本高、收益小,JDK 15 已废弃(JEP 374),这里作为历史概念了解即可。

synchronized 的好处是简单(写个关键字就完事,自动加锁释放),坏处是不可控(不能中断、不能超时、只能是非公平锁)。

2. ReentrantLock(基于 AQS)

ReentrantLockjava.util.concurrent.locks 里的显式锁,底层是 AQS(AbstractQueuedSynchronizer)

  • CAS 原子地改一个 state 变量抢锁(state=0 空闲,state=1 占用)
  • 抢不到就把线程放进一个等待队列,阻塞挂起
  • 释放锁时唤醒队列里的下一个线程

synchronized 强在:可重入、可中断(lockInterruptibly)、可超时(tryLock)、支持公平锁/非公平锁

synchronizedReentrantLock
用法关键字,自动加/释放显式 lock/unlock(要放 finally)
可重入
公平锁不支持支持(构造参数)
可中断/超时不支持支持
性能已优化得很好更灵活,场景复杂时用

四、数据库锁:悲观锁 vs 乐观锁

数据库层面也有锁,而且面试常问"悲观锁和乐观锁的区别"。

悲观锁:认为冲突一定会发生,先加锁再操作。典型是 SELECT ... FOR UPDATE(行锁),锁住后别人改不了,直到事务提交。

sql
SELECT stock FROM item WHERE id = 1 FOR UPDATE;  -- 锁住这一行
-- 扣减库存
UPDATE item SET stock = stock - 1 WHERE id = 1;

乐观锁:认为冲突很少发生,操作时不加锁,更新时检查"数据有没有被别人改过"。典型是版本号

sql
UPDATE item
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = #{oldVersion};   -- 版本变了就说明被别人改过,更新失败
悲观锁乐观锁
思路先锁后改先改后校验
实现FOR UPDATE、synchronized版本号、CAS、时间戳
适用冲突多、写多冲突少、读多
代价阻塞等待冲突时重试/失败

乐观锁的底层思想就是 CAS(比较并交换)——"我认为值还是 A,如果是就改成 B,不是就说明别人动过了"。Java 的 AtomicInteger 也是这个思路。

五、分布式锁三种实现

多台服务器要互斥,得靠一个"大家都能访问的公共第三方"。常见三种:

1. Redis(最常用)

SET NX EX 原子命令占锁,靠过期时间防死锁。优点是快、简单;缺点是不可重入、不自动续期、主从切换可能丢锁——这些 Redisson 都补上了。完整演进见我整理的《Redis 分布式锁详解》和《Redisson 实战》。

2. ZooKeeper(最可靠)

利用 临时顺序节点 + watch 机制:每个客户端创建一个带序号的临时节点,序号最小的拿到锁;没拿到锁的 watch 前一个节点,前一个释放(节点删除)后自己被唤醒。优点是可靠性高、天然可重入、自动释放(客户端挂了临时节点自动删);缺点是性能比 Redis 低、维护成本高

3. 数据库(最简单)

往一张表插一条唯一记录当锁(INSERT 成功 = 拿到锁),用完删掉。优点是不用额外组件;缺点是性能差、无过期机制(要自己加超时清理),一般只在没 Redis/ZK 时兜底用。

RedisZooKeeper数据库
性能
可靠性中(主从可能丢锁)
复杂度低(Redisson 开箱即用)
适用大多数场景对一致性要求极高无额外组件时兜底

六、其他常见的锁分类

除了上面这些,还有几个按"特性"划分的锁,概念上要知道:

  • 可重入锁 vs 不可重入锁:同一个线程能否重复加锁。synchronizedReentrantLock、Redisson 的 RLock 都可重入;裸 SETNX 不可重入。
  • 公平锁 vs 非公平锁:是否按"先来后到"顺序获取。公平锁按队列顺序,非公平锁谁抢到是谁的(默认是非公平,性能更好)。
  • 读写锁ReentrantReadWriteLock,读读不互斥、读写互斥、写写互斥,适合"读多写少"。
  • 自旋锁:抢不到锁时不阻塞,而是循环重试(CAS),适合"锁持有时间极短"的场景,避免线程切换开销。

七、怎么选(场景对照)

场景选什么
单机、简单互斥synchronized
单机、需要公平/中断/超时ReentrantLock
单机、读多写少ReentrantReadWriteLock
数据库、写多冲突多悲观锁(FOR UPDATE
数据库、读多冲突少乐观锁(版本号)
多机互斥、追求性能Redis 分布式锁(Redisson)
多机互斥、追求强一致ZooKeeper 分布式锁

小结

  • 锁的本质:保证"同一时刻只有一个执行单元访问共享资源"
  • 顶层分两类:本地锁(单 JVM,synchronized/ReentrantLock)和分布式锁(跨机器,Redis/ZK/DB)
  • 本地锁实现:synchronized 走锁升级(偏向→轻量级→重量级),ReentrantLock 走 AQS + CAS + 队列
  • 数据库锁分悲观锁(先锁后改,FOR UPDATE)和乐观锁(先改后校验,版本号/CAS)
  • 分布式锁三选:Redis(快)、ZooKeeper(可靠)、数据库(简单兜底)
  • 按特性还有:可重入、公平/非公平、读写锁、自旋锁

深入某个方向可以接着看:《Redis 分布式锁详解》讲 Redis 锁的完整演进,《Redisson 实战》讲生产级锁的实现,乐观锁在真实高并发下的用法看《秒杀系统架构分析》。