Java并发与数据库锁机制:悲观锁、乐观锁、隐式锁与显式锁
一、核心概念分类与对比
首先通过一张表总结四类锁的关键区别:
锁类型 | 核心思想 | 实现方式(典型工具) | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|---|
悲观锁 | 假设并发冲突一定会发生,访问数据前先加锁,阻止其他线程/事务同时修改 | Java:synchronized 、ReentrantLock ;数据库:SELECT ... FOR UPDATE | 写多读少、冲突概率高的场景(如库存扣减、账户余额更新) | 强一致性,实现简单 | 性能开销大(阻塞等待),可能死锁 |
乐观锁 | 假设并发冲突不常发生,不加锁直接操作,提交时检查数据是否被其他线程/事务修改过 | Java:AtomicInteger (CAS)、版本号机制(如version 字段);数据库:版本号或时间戳 | 读多写少、冲突概率低的场景(如商品浏览量统计、CMS内容更新) | 无阻塞,性能高 | 需处理冲突重试,ABA问题需额外解决 |
隐式锁 | 锁的获取和释放由系统/框架自动管理,开发者无需显式编写加锁/解锁代码 | Java:synchronized (JVM自动加锁/释放)、数据库:行锁(如InnoDB自动加锁) | 简单同步场景(如单方法内的线程安全控制、数据库的单行操作) | 代码简洁,不易遗漏 | 灵活性低,控制粒度粗 |
显式锁 | 开发者需要手动调用API获取锁和释放锁,通常提供更丰富的功能(如超时、可中断) | Java:ReentrantLock 、ReadWriteLock ;数据库:部分分布式锁需手动控制加锁逻辑 | 需要精细控制锁行为(如超时获取、公平锁、多条件队列)的场景 | 功能灵活,可定制化 | 代码复杂度高,需手动释放避免死锁 |
二、详细解析每类锁
1. 悲观锁(Pessimistic Lock)
核心思想
“先加锁,再访问” —— 认为并发冲突一定会发生,因此在操作数据前先获取锁,确保同一时间只有一个线程/事务能修改数据,其他线程/事务必须等待锁释放。
实现方式
Java层面:
synchronized
关键字(JVM内置悲观锁,自动管理锁的获取与释放)。ReentrantLock
(显式锁,需手动调用lock()
和unlock()
,支持超时、可中断等高级功能)。
数据库层面:
- **
SELECT ... FOR UPDATE
**:对查询的行记录加排他锁(其他事务无法修改或加锁,直到当前事务提交或回滚)。-- 事务1:对id=1的商品库存加锁 BEGIN; SELECT stock FROM products WHERE id = 1 FOR UPDATE; -- 加排他锁 UPDATE products SET stock = stock - 1 WHERE id = 1; -- 安全扣减 COMMIT;
- 行锁/表锁:InnoDB引擎默认对索引列加行锁(若无索引可能升级为表锁),MyISAM仅支持表锁。
- **
典型场景
- 写多读少:如秒杀库存扣减、银行账户余额转账(冲突概率高,需强一致性)。
- 事务性操作:数据库的多语句事务中保证数据修改的原子性(如订单创建+库存扣减)。
优缺点
- 优点:实现简单,保证强一致性(不会出现脏写)。
- 缺点:性能开销大(线程/事务阻塞等待),可能引发死锁(多个线程互相持有对方需要的锁)。
2. 乐观锁(Optimistic Lock)
核心思想
“先操作,再检查” —— 认为并发冲突不常发生,因此直接读取并修改数据,提交时通过版本号或CAS机制检查数据是否被其他线程/事务修改过,若冲突则重试或失败。
实现方式
Java层面:
- CAS(Compare-And-Swap):通过
AtomicInteger
、AtomicLong
等原子类的底层CAS指令(如Unsafe.compareAndSwapInt
)实现无锁并发。AtomicInteger stock = new AtomicInteger(100); // 尝试扣减库存(CAS保证原子性) boolean success = stock.compareAndSet(100, 99); // 当前值为100时才更新为99
- 版本号机制:在数据表中增加
version
字段,更新时检查版本是否匹配(类似CAS)。// 伪代码:更新商品信息时检查版本 int oldVersion = product.getVersion(); int affectedRows = update("UPDATE products SET name=?, version=version+1 WHERE id=? AND version=?", newName, productId, oldVersion); if (affectedRows == 0) {throw new OptimisticLockException("数据已被其他事务修改,请重试"); }
- CAS(Compare-And-Swap):通过
数据库层面:
- 版本号字段:表中增加
version
列(每次更新自增),事务提交时通过WHERE version=旧值
校验。 - 时间戳字段:用
update_time
代替版本号(精度依赖数据库时间)。
- 版本号字段:表中增加
典型场景
- 读多写少:如商品浏览量统计(频繁读取,偶尔更新)、CMS内容编辑(多人协作但冲突概率低)。
- 高并发但冲突少的场景:如用户信息更新(大部分请求不冲突,少数冲突时重试成本可接受)。
优缺点
- 优点:无阻塞,性能高(不占用锁资源);适合分布式环境(如Redis的乐观锁通过
WATCH
命令实现)。 - 缺点:需处理冲突重试(可能引发死循环),存在ABA问题(CAS中值从A→B→A,CAS无法感知中间变化,需用版本号或时间戳解决)。
3. 隐式锁(Implicit Lock)
核心思想
“自动管理” —— 锁的获取和释放由系统/运行时环境(如JVM、数据库引擎)自动完成,开发者无需显式编写加锁或解锁代码。
实现方式
Java层面:
synchronized
关键字:进入同步代码块时JVM自动加锁(通过对象监视器Monitor),退出时(正常return或异常抛出)自动释放锁。public synchronized void safeMethod() {// 进入方法时JVM自动加锁,退出时自动释放 }
数据库层面:
- 行锁/表锁:InnoDB引擎在执行更新/删除操作时,若条件命中索引,会自动对相关行加锁(开发者无需手动写
FOR UPDATE
,但需理解其隐式行为)。-- 隐式加行锁:更新id=1的记录时,InnoDB自动对id=1的行加排他锁 UPDATE products SET stock = stock - 1 WHERE id = 1;
- 行锁/表锁:InnoDB引擎在执行更新/删除操作时,若条件命中索引,会自动对相关行加锁(开发者无需手动写
典型场景
- 简单同步需求:单方法内的线程安全控制(如工具类的单例初始化)。
- 数据库的单语句操作:单条更新/删除语句(依赖索引时自动加行锁)。
优缺点
- 优点:代码简洁,不易遗漏锁释放(避免死锁)。
- 缺点:控制粒度粗(如
synchronized
锁住整个方法可能影响性能),灵活性低(无法定制超时、公平性等)。
4. 显式锁(Explicit Lock)
核心思想
“手动管理” —— 开发者需要显式调用API获取锁和释放锁,通常提供更丰富的功能(如超时等待、可中断、公平锁、多条件队列)。
实现方式
Java层面:
ReentrantLock
(可重入显式锁):需手动调用lock()
和unlock()
(通常配合try-finally
确保释放)。ReentrantLock lock = new ReentrantLock(); lock.lock(); // 手动加锁 try {// 临界区代码 } finally {lock.unlock(); // 必须手动释放! }
- 高级功能:支持超时获取锁(
tryLock(long timeout, TimeUnit unit)
)、可中断锁(lockInterruptibly()
)、公平锁(构造参数fair=true
)、多个条件变量(Condition
)。
数据库层面:
- 部分分布式锁(如基于Redis的Redlock算法)需开发者手动控制多节点加锁逻辑(非标准SQL功能)。
典型场景
- 需要精细控制锁行为:如限制锁等待时间(避免线程长时间阻塞)、公平性要求(按请求顺序获取锁)、多条件等待(如生产者-消费者模型中的不同队列)。
- 复杂并发逻辑:多个线程协作时(如线程池任务调度、资源池管理)。
优缺点
- 优点:功能灵活(支持超时、中断、公平性),可定制化程度高。
- 缺点:代码复杂度高(需手动释放锁,否则可能死锁),对开发者要求更高。
三、面试常见问题扩展
Q1:synchronized 是悲观锁还是乐观锁?是隐式锁还是显式锁?
- 答案:synchronized 是悲观锁(假设冲突会发生,先加锁再访问),同时也是隐式锁(锁的获取和释放由JVM自动管理,开发者无需手动操作)。
Q2:乐观锁一定比悲观锁性能好吗?
- 答案:不一定!乐观锁在冲突概率低时性能更高(无阻塞),但在冲突频繁时因重试次数增加可能导致整体性能下降;悲观锁在冲突高时能直接避免竞争,但会阻塞其他线程。需根据业务场景选择。
Q3:数据库的乐观锁如何避免ABA问题?
- 答案:通过版本号(version)而非数据本身值来判断变化。每次更新时版本号+1,即使数据的值从A→B→A,版本号也会递增(如1→2→3),通过检查版本号是否匹配可感知中间修改。
Q4:Redis分布式锁是悲观锁还是乐观锁?
- 答案:Redis分布式锁本质是悲观锁(通过
SET key value NX PX timeout
加锁后,其他客户端无法同时获取同一把锁),但它是基于Redis的乐观锁思想(如WATCH
命令监控键变化)的扩展应用。
四、总结表格(快速回顾)
锁类型 | 思想 | 实现方式(Java示例) | 典型场景 | 核心特点 |
---|---|---|---|---|
悲观锁 | 先加锁再访问 | synchronized 、ReentrantLock 、SELECT ... FOR UPDATE | 写多读少、强一致性需求 | 强一致,可能阻塞/死锁 |
乐观锁 | 先操作后检查 | AtomicInteger (CAS)、版本号机制 | 读多写少、低冲突场景 | 无阻塞,需处理冲突重试 |
隐式锁 | 自动管理 | synchronized (JVM自动加锁)、数据库隐式行锁 | 简单同步、单语句操作 | 代码简洁,控制粒度粗 |
显式锁 | 手动管理 | ReentrantLock 、分布式锁手动控制 | 复杂并发逻辑、精细控制需求 | 功能灵活,需手动释放 |
掌握这些锁的核心逻辑,不仅能应对秋招面试中的高频问题(如“synchronized和ReentrantLock的区别”“乐观锁如何实现”“分布式锁的选型”),还能在实际开发中根据业务场景选择最合适的并发控制策略。建议结合代码Demo(如用CAS实现计数器、用synchronized
保护共享资源)加深理解!