乐观锁 与 悲观锁 笔记251007
乐观锁 与 悲观锁 笔记251007
乐观锁与悲观锁详解
基本概念
悲观锁 (Pessimistic Locking)
思想:假设最坏的情况会发生,每次访问数据时都认为其他线程会修改数据,因此在访问数据前先加锁。
特点:
- 先加锁,后操作
- 适合写操作多的场景
- 保证数据强一致性
乐观锁 (Optimistic Locking)
思想:假设最好的情况会发生,每次访问数据时认为其他线程不会修改数据,只在提交更新时检查数据是否被修改过。
特点:
- 先操作,后检查
- 适合读操作多的场景
- 通过版本控制实现
实现机制对比
悲观锁实现方式
数据库层面
-- MySQL 行级锁
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;-- SQL Server
BEGIN TRAN;
SELECT * FROM users WITH (UPDLOCK) WHERE id = 1;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT TRAN;
Java 层面
// synchronized 关键字
public class PessimisticLockExample {private int balance = 1000;public synchronized void withdraw(int amount) {if (balance >= amount) {balance -= amount;}}
}// ReentrantLock
public class PessimisticLockExample2 {private int balance = 1000;private final ReentrantLock lock = new ReentrantLock();public void withdraw(int amount) {lock.lock();try {if (balance >= amount) {balance -= amount;}} finally {lock.unlock();}}
}
乐观锁实现方式
数据库层面(版本号机制)
-- 添加版本号字段
CREATE TABLE users (id BIGINT PRIMARY KEY,name VARCHAR(50),balance DECIMAL(10,2),version INT DEFAULT 0
);-- 更新时检查版本号
UPDATE users
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 1;-- 检查受影响的行数,如果为0说明版本冲突
Java 层面(CAS 操作)
// 使用 Atomic 类
public class OptimisticLockExample {private AtomicInteger balance = new AtomicInteger(1000);public boolean withdraw(int amount) {int current, newValue;do {current = balance.get();if (current < amount) {return false; // 余额不足}newValue = current - amount;} while (!balance.compareAndSet(current, newValue));return true;}
}// 使用版本号的对象封装
public class VersionedAccount {private static class AccountState {final int balance;final int version;AccountState(int balance, int version) {this.balance = balance;this.version = version;}}private final AtomicReference<AccountState> state = new AtomicReference<>(new AccountState(1000, 0));public boolean withdraw(int amount) {AccountState current, newState;do {current = state.get();if (current.balance < amount) {return false;}newState = new AccountState(current.balance - amount, current.version + 1);} while (!state.compareAndSet(current, newState));return true;}
}
详细对比分析
适用场景对比
场景 | 悲观锁 | 乐观锁 |
---|---|---|
写操作频率 | 高(>50%) | 低(<20%) |
数据竞争程度 | 激烈 | 轻微 |
读/写比例 | 写多读少 | 读多写少 |
响应时间要求 | 实时性要求高 | 可接受重试 |
系统开销 | 锁管理开销大 | 冲突检测开销小 |
性能特点对比
public class LockPerformanceTest {private static final int THREAD_COUNT = 10;private static final int OPERATION_COUNT = 10000;// 悲观锁实现static class PessimisticCounter {private long count = 0;private final Object lock = new Object();public void increment() {synchronized(lock) {count++;}}public long getCount() {synchronized(lock) {return count;}}}// 乐观锁实现static class OptimisticCounter {private AtomicLong count = new AtomicLong(0);public void increment() {long oldValue, newValue;do {oldValue = count.get();newValue = oldValue + 1;} while (!count.compareAndSet(oldValue, newValue));}public long getCount() {return count.get();}}public static void testPerformance() {// 测试代码...}
}
冲突处理机制
悲观锁冲突处理
public class PessimisticConflictResolution {private final Map<String, Object> locks = new ConcurrentHashMap<>();public void transfer(String fromAccount, String toAccount, int amount) {// 按固定顺序获取锁,避免死锁Object lock1 = locks.computeIfAbsent(fromAccount, k -> new Object());Object lock2 = locks.computeIfAbsent(toAccount, k -> new Object());Object firstLock, secondLock;if (fromAccount.compareTo(toAccount) < 0) {firstLock = lock1;secondLock = lock2;} else {firstLock = lock2;secondLock = lock1;}synchronized(firstLock) {synchronized(secondLock) {// 执行转账操作performTransfer(fromAccount, toAccount, amount);}}}
}
乐观锁冲突处理
public class OptimisticConflictResolution {private final Map<String, AtomicInteger> accounts = new ConcurrentHashMap<>();public boolean transfer(String fromAccount, String toAccount, int amount) {int retries = 3; // 最大重试次数for (int i = 0; i < retries; i++) {int fromBalance = accounts.get(fromAccount).get();int toBalance = accounts.get(toAccount).get();if (fromBalance < amount) {return false; // 余额不足}// 尝试原子更新boolean fromSuccess = accounts.get(fromAccount).compareAndSet(fromBalance, fromBalance - amount);boolean toSuccess = accounts.get(toAccount).compareAndSet(toBalance, toBalance + amount);if (fromSuccess && toSuccess) {return true; // 转账成功}// 冲突发生,回滚并重试if (fromSuccess && !toSuccess) {accounts.get(fromAccount).compareAndSet(fromBalance - amount, fromBalance);}try {Thread.sleep(10 * (i + 1)); // 指数退避} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}return false; // 重试次数用尽}
}
实际应用案例
电商库存管理
悲观锁方案
public class InventoryPessimistic {private final Map<Long, Integer> inventory = new HashMap<>();private final Map<Long, Object> itemLocks = new ConcurrentHashMap<>();public boolean deductStock(Long itemId, int quantity) {Object lock = itemLocks.computeIfAbsent(itemId, k -> new Object());synchronized(lock) {Integer stock = inventory.get(itemId);if (stock == null || stock < quantity) {return false;}inventory.put(itemId, stock - quantity);return true;}}
}
乐观锁方案
public class InventoryOptimistic {private final ConcurrentHashMap<Long, AtomicInteger> inventory = new ConcurrentHashMap<>();public boolean deductStock(Long itemId, int quantity) {AtomicInteger stock = inventory.get(itemId);if (stock == null) {return false;}int current, newValue;do {current = stock.get();if (current < quantity) {return false;}newValue = current - quantity;} while (!stock.compareAndSet(current, newValue));return true;}
}
银行账户系统
public class BankAccount {// 悲观锁版本public static class PessimisticBankAccount {private BigDecimal balance;private final Object lock = new Object();public boolean transferTo(PessimisticBankAccount to, BigDecimal amount) {// 获取锁的顺序很重要Object firstLock = this.lock;Object secondLock = to.lock;if (System.identityHashCode(this) > System.identityHashCode(to)) {firstLock = to.lock;secondLock = this.lock;}synchronized(firstLock) {synchronized(secondLock) {if (balance.compareTo(amount) < 0) {return false;}this.balance = this.balance.subtract(amount);to.balance = to.balance.add(amount);return true;}}}}// 乐观锁版本public static class OptimisticBankAccount {private AtomicReference<BigDecimal> balance = new AtomicReference<>(BigDecimal.ZERO);public boolean transferTo(OptimisticBankAccount to, BigDecimal amount) {while (true) {BigDecimal currentFrom = balance.get();BigDecimal currentTo = to.balance.get();if (currentFrom.compareTo(amount) < 0) {return false;}BigDecimal newFrom = currentFrom.subtract(amount);BigDecimal newTo = currentTo.add(amount);if (balance.compareAndSet(currentFrom, newFrom)) {if (to.balance.compareAndSet(currentTo, newTo)) {return true;} else {// 回滚balance.compareAndSet(newFrom, currentFrom);}}}}}
}
选择策略指南
选择悲观锁的情况
- 数据竞争激烈:多个线程频繁修改同一数据
- 事务成功率要求高:不能接受重试失败
- 操作复杂度高:涉及多个数据项的复杂事务
- 实时性要求严格:需要立即获得执行结果
选择乐观锁的情况
- 读多写少:大部分操作是读取,少量写入
- 数据竞争不激烈:冲突概率较低
- 系统性能要求高:希望减少锁开销
- 可接受重试:业务上允许操作失败重试
混合策略
public class HybridLockStrategy {private final ReentrantLock pessimisticLock = new ReentrantLock();private final AtomicInteger version = new AtomicInteger(0);private String data;// 读操作使用乐观锁public String readData() {int startVersion = version.get();String currentData = data;// 检查版本是否变化(读一致性检查)if (version.get() == startVersion) {return currentData;}// 版本变化,使用悲观锁重读pessimisticLock.lock();try {return data;} finally {pessimisticLock.unlock();}}// 写操作使用悲观锁public void writeData(String newData) {pessimisticLock.lock();try {data = newData;version.incrementAndGet();} finally {pessimisticLock.unlock();}}
}
总结
特性 | 悲观锁 | 乐观锁 |
---|---|---|
并发性能 | 低(阻塞其他线程) | 高(无阻塞) |
数据一致性 | 强一致性 | 最终一致性 |
实现复杂度 | 简单 | 复杂(需处理冲突) |
系统开销 | 锁管理开销大 | 冲突检测开销小 |
适用场景 | 写多读少,竞争激烈 | 读多写少,竞争少 |
失败处理 | 线程阻塞等待 | 操作重试或放弃 |
最佳实践建议:
- 根据业务场景的特点选择合适的锁策略
- 在高并发系统中,优先考虑乐观锁+重试机制
- 在数据强一致性要求的场景,使用悲观锁更可靠
- 考虑使用混合策略,平衡性能与一致性需求
CAS
是典型的乐观锁, synchronized
是典型悲观锁
1. synchronized - 典型的悲观锁
工作原理
synchronized
关键字在 Java 中实现了典型的悲观锁机制:
public class PessimisticExample {private int count = 0;// synchronized 方法 - 悲观锁public synchronized void increment() {count++; // 执行前先获取锁,确保独占访问}// synchronized 代码块 - 悲观锁 public void decrement() {synchronized(this) { // 先加锁,再操作count--;}}
}
为什么是悲观锁?
- “先加锁,后操作”:线程在执行临界区代码前,必须先获得锁
- 假设冲突会发生:认为其他线程很可能会同时修改数据,所以提前预防
- 阻塞等待:如果锁被占用,其他线程会被挂起(阻塞),直到锁释放
- 独占访问:确保同一时刻只有一个线程能访问受保护的资源
执行流程
线程进入 synchronized 块↓
尝试获取锁 → 失败:进入阻塞状态,等待唤醒↓ 成功
执行临界区代码↓
释放锁,唤醒等待线程
2. CAS - 典型的乐观锁
工作原理
CAS 操作体现了乐观锁的核心思想:
import java.util.concurrent.atomic.AtomicInteger;public class OptimisticExample {private AtomicInteger count = new AtomicInteger(0);// CAS 操作 - 乐观锁public void increment() {int current, next;do {current = count.get(); // 1. 读取当前值next = current + 1; // 2. 计算新值// 3. 尝试更新:如果值没变则更新,否则重试} while (!count.compareAndSet(current, next));}
}
为什么是乐观锁?
- “先操作,后检查”:先进行计算,提交时检查是否有冲突
- 假设冲突很少发生:认为在大多数情况下不会有其他线程干扰
- 非阻塞:CAS 失败时线程不会阻塞,而是立即重试或处理失败
- 无锁编程:不需要获取和释放锁的开销
执行流程
读取当前值和版本号↓
执行业务逻辑计算新值↓
尝试CAS更新 → 失败:重试或处理冲突↓ 成功
更新完成
3. 对比表格
特性 | synchronized (悲观锁) | CAS (乐观锁) |
---|---|---|
加锁时机 | 操作前先加锁 | 操作后检查冲突 |
线程状态 | 获取不到锁时阻塞 | CAS失败时自旋重试 |
性能特点 | 高竞争下相对稳定 | 低竞争下性能极佳 |
CPU使用 | 线程阻塞,CPU可调度其他任务 | 自旋重试,可能消耗CPU |
实现复杂度 | 简单易用 | 需要处理ABA问题和重试逻辑 |
适用场景 | 写操作频繁、临界区复杂 | 读多写少、低冲突场景 |
4. 实际性能对比
让我们通过一个计数器示例来对比两者的性能差异:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.CountDownLatch;public class LockComparison {private int syncCount = 0;private AtomicInteger casCount = new AtomicInteger(0);private static final int THREAD_COUNT = 100;private static final int PER_THREAD_INCREMENTS = 100000;// synchronized 计数器public synchronized void syncIncrement() {syncCount++;}// CAS 计数器public void casIncrement() {casCount.getAndIncrement();}public static void main(String[] args) throws InterruptedException {LockComparison demo = new LockComparison();// 测试 synchronizedlong startTime = System.currentTimeMillis();CountDownLatch latch1 = new CountDownLatch(THREAD_COUNT);for (int i = 0; i < THREAD_COUNT; i++) {new Thread(() -> {for (int j = 0; j < PER_THREAD_INCREMENTS; j++) {demo.syncIncrement();}latch1.countDown();}).start();}latch1.await();long syncTime = System.currentTimeMillis() - startTime;// 测试 CASstartTime = System.currentTimeMillis();CountDownLatch latch2 = new CountDownLatch(THREAD_COUNT);for (int i = 0; i < THREAD_COUNT; i++) {new Thread(() -> {for (int j = 0; j < PER_THREAD_INCREMENTS; j++) {demo.casIncrement();}latch2.countDown();}).start();}latch2.await();long casTime = System.currentTimeMillis() - startTime;System.out.println("synchronized 结果: " + demo.syncCount + ", 耗时: " + syncTime + "ms");System.out.println("CAS 结果: " + demo.casCount.get() + ", 耗时: " + casTime + "ms");}
}
典型输出(在低竞争环境下):
synchronized 结果: 10000000, 耗时: 1250ms
CAS 结果: 10000000, 耗时: 680ms
5. 现代并发库中的应用
synchronized 的演进
JDK 1.6 之后,synchronized
进行了大量优化:
- 偏向锁:减少无竞争时的开销
- 轻量级锁:使用 CAS 进行锁竞争
- 锁膨胀:竞争激烈时升级为重量级锁
CAS 的广泛应用
// java.util.concurrent 包中大量使用 CAS
AtomicInteger, AtomicLong, AtomicReference
ConcurrentHashMap // JDK 1.8 使用 CAS + synchronized
CopyOnWriteArrayList
LongAdder // 分段 CAS,超高并发性能更好
6. 选择建议
使用 synchronized 的情况:
- 代码简单,不想处理复杂的并发控制
- 临界区代码执行时间较长
- 写操作频繁,竞争激烈
- 需要保证操作的绝对原子性
使用 CAS 的情况:
- 读多写少的场景
- 对性能要求极高
- 愿意处理 ABA 问题和重试逻辑
- 简单的原子操作(如计数器、状态标志)
混合使用:
现代并发库经常混合使用两种策略:
// ConcurrentHashMap 中的实现思路
if (CAS操作成功) {// 快速路径
} else {synchronized (锁对象) {// 慢速路径,处理复杂情况}
}
总结
您的判断完全正确:
synchronized
是悲观锁:采取"先加锁,后操作"的保守策略,防止冲突发生CAS
是乐观锁:采取"先操作,后检查"的积极策略,相信冲突很少发生
理解这个区别对于编写高性能的并发程序至关重要。在实际开发中,应该根据具体的业务场景和性能要求来选择合适的并发控制策略。
乐观锁有哪些? 悲观锁有哪些?
好的,这是一个非常实际的问题。我们来详细列举常见的乐观锁和悲观锁实现。
一、乐观锁的实现
乐观锁并不是一个具体的锁,而是一种思想或策略。它的核心是 “版本控制” 和 “冲突检测” 。因此,它的实现通常表现为以下几种技术:
1. 版本号机制
这是最经典的乐观锁实现方式,尤其在数据库层面。
- 实现方法:在数据表里增加一个
version
字段(或时间戳字段)。 - 操作流程:
- 读取数据时,同时获取
version
值(假设为1
)。 - 更新数据时,执行
UPDATE ... SET ..., version = version + 1 WHERE id = ? AND version = 1
。 - 检查此次
UPDATE
操作影响的行数。如果为 0,说明在更新之前数据已经被其他线程修改过了(version
已经不是1
),本次更新失败。
- 读取数据时,同时获取
- 应用场景:所有支持 SQL 的数据库,如 MySQL、PostgreSQL、Oracle 等。广泛应用于业务系统的数据更新中。
2. CAS 算法
CAS 是乐观锁思想最直接、最核心的技术体现。
- 实现方法:通过底层硬件的原子指令(如
CMPXCHG
)实现。 - 应用场景:
- Java
java.util.concurrent.atomic
包:AtomicInteger
AtomicLong
AtomicBoolean
AtomicReference
AtomicStampedReference
(解决了 ABA 问题)AtomicMarkableReference
(解决了 ABA 问题)
- Java AQS:
AbstractQueuedSynchronizer
是 Java 并发包中锁和同步器的基石,其内部大量使用了 CAS 来原子性地更新状态。 - 并发集合:如
ConcurrentHashMap
在 JDK 8 之后的分段锁和节点操作中也大量使用了 CAS。
- Java
3. 分布式系统中的乐观锁
在分布式环境中,乐观锁同样适用。
- 实现方法:
- 数据库版本号:同上,是最通用的方式。
- Redis WATCH/MULTI/EXEC:Redis 的事务机制结合
WATCH
命令可以实现乐观锁。WATCH
监控一个或多个键,如果在EXEC
执行前这些键被修改,则整个事务会失败。 - ETag (HTTP):在 RESTful API 中,客户端获取资源时服务端返回一个
ETag
(通常是资源的哈希值或版本号)。客户端更新资源时,在If-Match
头中带上这个ETag
。服务端验证ETag
是否匹配,以此实现乐观并发控制。
二、悲观锁的实现
悲观锁是具体的 “锁” 实体,它的核心思想是 “先加锁,再访问” 。因此,它的实现就是各种我们日常所说的"锁"。
1. 在编程语言层面
-
Java
synchronized
关键字:- 可以修饰实例方法(锁是当前实例对象)。
- 可以修饰静态方法(锁是当前类的 Class 对象)。
- 可以修饰代码块(需要显式指定锁对象)。
- 这是 Java 中最基本、最常用的内置悲观锁。
-
Java
java.util.concurrent.locks.Lock
接口及其实现:ReentrantLock
:可重入互斥锁,功能比synchronized
更丰富(如可中断、可超时、公平锁等)。ReentrantReadWriteLock.ReadLock
/ReentrantReadWriteLock.WriteLock
:读写锁。写锁是悲观锁,读锁是共享锁。
2. 在数据库层面
数据库提供了多种粒度的悲观锁。
- 行级锁:
SELECT ... FOR UPDATE
:这是最常用的 SQL 级悲观锁。它会在查询到的行上加上排他锁,直到事务结束才释放。其他事务无法修改或对这些行再加FOR UPDATE
锁。- 应用场景:高并发下的余额扣减、库存扣减等。
- 表级锁:
LOCK TABLES ... WRITE/READ
:锁定整张表,粒度大,并发性差,一般很少使用。
3. 分布式系统中的悲观锁
当多个进程/服务需要互斥地访问共享资源时,需要使用分布式悲观锁。
- 基于 Redis 的分布式锁:
- 使用
SET key value NX PX timeout
命令(或 Redisson 库)来实现。NX
表示只有当 key 不存在时才设置,PX
设置过期时间防止死锁。 - 应用场景:集群环境下的定时任务调度、全局唯一操作等。
- 使用
- 基于 ZooKeeper 的分布式锁:
- 利用 ZooKeeper 的临时有序节点。每个客户端都在某个持久节点下创建一个临时有序节点,判断自己是否是最小序号的节点,如果是则获得锁。
- 应用场景:对可靠性要求极高的分布式协调场景。
- 基于 Etcd 的分布式锁:原理与 ZooKeeper 类似。
总结表格
类别 | 实现方式 | 具体例子 |
---|---|---|
乐观锁 | 版本号机制 | 数据库表增加 version 字段;REST API 的 ETag |
CAS 算法 | AtomicInteger , ConcurrentHashMap (部分操作), AQS | |
分布式冲突检测 | Redis WATCH 命令 | |
悲观锁 | 编程语言内置锁 | Java synchronized 关键字 |
显式锁 API | Java ReentrantLock , ReentrantReadWriteLock.WriteLock | |
数据库锁 | SELECT ... FOR UPDATE (行锁), LOCK TABLES (表锁) | |
分布式锁 | 基于 Redis, ZooKeeper, Etcd 实现的分布式锁 |
如何选择?
- 追求极致性能、读多写少、冲突概率低 -> 优先考虑乐观锁(如 CAS 或版本号)。
- 写操作频繁、冲突概率高、业务逻辑复杂且重试成本高 -> 优先考虑悲观锁(如
synchronized
、ReentrantLock
或SELECT ... FOR UPDATE
)。 - 在分布式系统中需要对共享资源进行互斥访问 -> 使用分布式悲观锁(如 Redis 锁)。
哪些是乐观锁? 哪些是悲观锁?
乐观锁与悲观锁分类详解
常见的悲观锁实现
数据库层面的悲观锁
-- MySQL 行级锁
SELECT * FROM table WHERE id = 1 FOR UPDATE;
SELECT * FROM table WHERE id = 1 LOCK IN SHARE MODE;-- SQL Server
SELECT * FROM table WITH (UPDLOCK) WHERE id = 1;
SELECT * FROM table WITH (ROWLOCK, XLOCK) WHERE id = 1;-- Oracle
SELECT * FROM table WHERE id = 1 FOR UPDATE;
Java 中的悲观锁
// 1. synchronized 关键字
public synchronized void method() { }
private static synchronized void staticMethod() { }// 2. ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {// 临界区代码
} finally {lock.unlock();
}// 3. ReadWriteLock 写锁
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock writeLock = rwLock.writeLock();// 4. 集合类的同步包装器
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
其他系统的悲观锁
# <font size=5 color=black ><b>Python threading.Lock</b></font>
import threading
lock = threading.Lock()
with lock:# <font size=5 color=black ><b>临界区代码</b></font># <font size=5 color=black ><b>Redis 分布式锁</b></font>
SET resource_name random_value NX PX 30000
常见的乐观锁实现
数据库层面的乐观锁
-- 版本号机制
UPDATE table
SET column1 = value1, version = version + 1
WHERE id = 1 AND version = current_version;-- 时间戳机制
UPDATE table
SET column1 = value1, update_time = CURRENT_TIMESTAMP
WHERE id = 1 AND update_time = last_update_time;
Java 中的乐观锁
// 1. Atomic 原子类 (基于 CAS)
AtomicInteger atomicInt = new AtomicInteger(0);
AtomicLong atomicLong = new AtomicLong(0L);
AtomicReference<String> atomicRef = new AtomicReference<>("hello");// 2. AtomicStampedReference (解决 ABA 问题)
AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(0, 0);// 3. AtomicFieldUpdater
AtomicIntegerFieldUpdater<MyClass> updater = AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "count");// 4. LongAdder (高并发计数)
LongAdder adder = new LongAdder();
adder.increment();
Java 并发包中的乐观锁实现
// ConcurrentHashMap (分段锁 + CAS)
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();// CopyOnWriteArrayList (写时复制)
CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();// ConcurrentLinkedQueue (无锁队列)
ConcurrentLinkedQueue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
详细分类列表
明确的悲观锁
类别 | 具体实现 | 说明 |
---|---|---|
数据库锁 | SELECT ... FOR UPDATE | 行级排他锁 |
SELECT ... LOCK IN SHARE MODE | 行级共享锁 | |
表锁 (LOCK TABLES ) | 表级锁 | |
Java 同步 | synchronized 关键字 | 内置监视器锁 |
ReentrantLock | 可重入锁 | |
ReentrantReadWriteLock | 读写锁 | |
StampedLock 的写模式 | 邮票锁的悲观读 | |
分布式锁 | Redis SET NX PX | 基于 Redis 的分布式锁 |
ZooKeeper 顺序节点 | 基于 ZooKeeper 的分布式锁 | |
数据库分布式锁 | 基于数据库的分布式锁 |
明确的乐观锁
类别 | 具体实现 | 说明 |
---|---|---|
CAS 操作 | AtomicInteger , AtomicLong | 基于 CAS 的原子类 |
AtomicReference | 引用类型原子类 | |
AtomicStampedReference | 带版本号的原子引用 | |
AtomicMarkableReference | 带标记的原子引用 | |
数据库版本控制 | 版本号字段 + CAS 更新 | 通过版本号避免冲突 |
时间戳字段 + CAS 更新 | 通过时间戳避免冲突 | |
无锁数据结构 | ConcurrentLinkedQueue | 无锁队列 |
ConcurrentSkipListMap | 无锁跳表 | |
CopyOnWriteArrayList | 写时复制列表 | |
Java 8+ | LongAdder , DoubleAdder | 高并发累加器 |
CompletableFuture | 异步编程中的乐观控制 |
混合型或需要具体分析的情况
根据使用方式决定
// StampedLock 可以根据使用方式决定是乐观还是悲观
StampedLock lock = new StampedLock();// 乐观读
long stamp = lock.tryOptimisticRead();
if (!lock.validate(stamp)) {// 如果乐观读失败,升级为悲观读stamp = lock.readLock();try {// 读取数据} finally {lock.unlockRead(stamp);}
}// 悲观写
long writeStamp = lock.writeLock();
try {// 写入数据
} finally {lock.unlockWrite(writeStamp);
}
数据库隔离级别的影响
-- 在 READ COMMITTED 隔离级别下,某些查询可能表现出乐观锁特性
-- 在 REPEATABLE READ 隔离级别下,某些查询可能表现出悲观锁特性
快速判断方法
判断是否为悲观锁的方法:
- 是否在操作前主动获取锁? ✓
- 是否阻塞其他线程访问? ✓
- 是否假设冲突经常发生? ✓
判断是否为乐观锁的方法:
- 是否在提交时检查冲突? ✓
- 是否使用版本号或CAS机制? ✓
- 冲突时是否重试或失败? ✓
- 是否假设冲突很少发生? ✓
实际应用中的选择建议
使用悲观锁的场景:
// 银行转账 - 必须保证强一致性
@Transactional
public void transfer(Long fromAccount, Long toAccount, BigDecimal amount) {// 使用悲观锁锁定账户Account from = accountRepository.findByIdWithLock(fromAccount);Account to = accountRepository.findByIdWithLock(toAccount);// 执行转账逻辑from.debit(amount);to.credit(amount);
}
使用乐观锁的场景:
// 商品库存扣减 - 允许部分失败,重试机制
public boolean deductStock(Long productId, Integer quantity) {for (int i = 0; i < 3; i++) { // 重试3次Product product = productRepository.findById(productId);if (product.getStock() < quantity) {return false;}int result = productRepository.updateStock(productId, quantity, product.getVersion());if (result > 0) {return true; // 更新成功}}return false; // 重试后仍失败
}
总结
悲观锁的核心思想是"先加锁,再操作",包括:
- 数据库的
FOR UPDATE
、WITH (UPDLOCK)
- Java 的
synchronized
、ReentrantLock
- 各种分布式锁实现
乐观锁的核心思想是"先操作,提交时检查",包括:
- 数据库版本号机制
- Java 的
AtomicXXX
类 - CAS 操作和无锁数据结构
- 各种重试机制
在实际项目中,应该根据业务场景、并发程度和数据一致性要求来选择合适的锁机制。
1️⃣ 参考 1
我们来详细探讨乐观锁与悲观锁这两种重要的并发控制策略。
1. 核心思想对比
悲观锁 (Pessimistic Locking)
思想:假设最坏的情况会发生。“我认为只要我访问数据,别人就很可能也会来修改它,所以我要先加锁,确保只有我能操作。”
比喻:
- 就像你把日记本锁在抽屉里,认为只要拿出来别人就可能会看
- 就像停车场只有一个出口,车辆必须排队依次通过
乐观锁 (Optimistic Locking)
思想:假设最好的情况会发生。“我认为别人不太会在我操作的时候修改数据,所以我不加锁,先操作,提交时再检查有没有冲突。”
比喻:
- 就像多人编辑在线文档,各自编辑自己的副本,最后合并时解决冲突
- 就像Git版本控制,各自提交代码,合并时处理冲突
2. 实现机制对比
悲观锁的实现方式
数据库层面:
-- 使用 SELECT ... FOR UPDATE 锁定记录
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 悲观锁
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
Java 层面:
// 使用 synchronized
public synchronized void transfer(int from, int to, int amount) {// 转账操作
}// 使用 ReentrantLock
private final Lock lock = new ReentrantLock();public void transfer(int from, int to, int amount) {lock.lock();try {// 转账操作} finally {lock.unlock();}
}
乐观锁的实现方式
数据库层面(版本号机制):
-- 添加 version 字段
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 123 AND version = 5; -- 检查版本号-- 如果受影响行数为0,说明版本号已变化,需要重试
Java 层面(CAS 操作):
// 使用 AtomicInteger
private AtomicInteger balance = new AtomicInteger(1000);public boolean withdraw(int amount) {int current, next;do {current = balance.get();if (current < amount) {return false; // 余额不足}next = current - amount;} while (!balance.compareAndSet(current, next)); // CAS操作return true;
}// 使用 AtomicStampedReference 解决ABA问题
private AtomicStampedReference<Integer> account = new AtomicStampedReference<>(1000, 0);public boolean transfer(int amount) {int[] stampHolder = new int[1];int current, next, currentStamp, nextStamp;do {current = account.get(stampHolder);currentStamp = stampHolder[0];if (current < amount) return false;next = current - amount;nextStamp = currentStamp + 1;} while (!account.compareAndSet(current, next, currentStamp, nextStamp));return true;
}
3. 工作流程对比
悲观锁工作流程
1. 申请锁↓
2. 获取锁成功? → 失败:阻塞等待↓ 成功
3. 执行业务操作↓
4. 提交事务,释放锁
乐观锁工作流程
1. 读取数据(包括版本号)↓
2. 执行业务逻辑(计算新值)↓
3. 尝试更新(检查版本号)↓
4. 版本一致? → 否:重试或失败处理↓ 是
5. 更新成功
4. 优缺点分析
悲观锁的优点
- ✅ 简单可靠:逻辑直接,容易理解
- ✅ 强一致性:保证数据操作的绝对串行化
- ✅ 避免冲突:从根本上防止并发冲突
- ✅ 适合长事务:对于执行时间长的操作很有效
悲观锁的缺点
- ❌ 性能开销:加锁、释放锁需要额外开销
- ❌ 死锁风险:可能产生死锁,需要额外处理
- ❌ 并发度低:阻塞其他线程,降低系统吞吐量
- ❌ ** scalability 差**:不适合高并发场景
乐观锁的优点
- ✅ 高并发:读操作不会被阻塞,提高吞吐量
- ✅ 无死锁:不使用锁,避免死锁问题
- ✅ 性能好:在低冲突场景下性能优异
- ✅ 适合读多写少:大多数场景都是读多写少
乐观锁的缺点
- ❌ ABA 问题:需要额外机制解决
- ❌ 高冲突时性能差:大量重试消耗 CPU
- ❌ 实现复杂:需要处理重试逻辑和冲突解决
- ❌ 不适合长事务:重试次数可能很多
5. 适用场景
适合使用悲观锁的场景
- 写操作频繁:冲突发生概率很高的场景
- 临界区复杂:操作执行时间较长
- 数据一致性要求极高:不允许任何冲突发生
- 简单业务逻辑:不想处理复杂的重试机制
具体例子:
- 银行核心系统的账户余额修改
- 库存管理中稀缺商品的抢购
- 票务系统的座位锁定
适合使用乐观锁的场景
- 读多写少:大部分操作是读取,很少修改
- 低冲突环境:并发写入的概率较低
- 高性能要求:需要支持高并发访问
- 短事务:业务操作执行时间短
具体例子:
- 社交媒体的点赞计数
- 文章的阅读量统计
- 用户个人信息的更新
- 购物车的商品数量修改
6. 实战示例
库存管理的两种实现
悲观锁实现:
@Service
public class InventoryServicePessimistic {@Transactionalpublic boolean deductStock(Long productId, Integer quantity) {// 悲观锁:查询时锁定记录Product product = productRepository.findByIdWithLock(productId);if (product.getStock() >= quantity) {product.setStock(product.getStock() - quantity);productRepository.save(product);return true;}return false;}
}
乐观锁实现:
@Service
public class InventoryServiceOptimistic {public boolean deductStock(Long productId, Integer quantity) {int retryCount = 0;final int MAX_RETRY = 3;while (retryCount < MAX_RETRY) {Product product = productRepository.findById(productId);if (product.getStock() < quantity) {return false;}int updated = productRepository.updateStockWithVersion(productId, quantity, product.getVersion());if (updated > 0) {return true; // 更新成功}retryCount++;// 可选:加入指数退避try { Thread.sleep(10 * retryCount); } catch (InterruptedException e) { break; }}return false; // 重试多次后失败}
}
7. 总结对比表格
特性 | 悲观锁 | 乐观锁 |
---|---|---|
核心思想 | 先加锁,后操作 | 先操作,后检查 |
实现方式 | synchronized 、ReentrantLock 、SELECT FOR UPDATE | CAS、版本号、时间戳 |
性能 | 低并发下较好,高并发下差 | 低冲突下极好,高冲突下差 |
数据一致性 | 强一致性 | 最终一致性 |
死锁 | 可能发生 | 不会发生 |
适用场景 | 写多读少、高冲突、长事务 | 读多写少、低冲突、短事务 |
开发复杂度 | 简单 | 相对复杂 |
扩展性 | 较差 | 较好 |
8. 选择建议
- 默认选择乐观锁:在大多数现代应用中,读操作远多于写操作,乐观锁是更好的选择
- 冲突频繁用悲观锁:当业务逻辑注定会产生大量冲突时,使用悲观锁
- 考虑业务特性:根据数据的重要性和一致性要求做出选择
- 可以混合使用:在同一系统中,不同模块可以根据特性使用不同的锁策略
现代趋势:随着分布式系统和微服务的普及,基于版本的乐观锁机制越来越受欢迎,因为它更适合分布式环境,能提供更好的扩展性和性能。
2️⃣ 参考 2
我们来详细解析乐观锁和悲观锁这两种重要的并发控制策略。
一、核心思想对比
这两种锁的核心区别在于它们对于数据冲突可能性的预期。
特性 | 悲观锁 | 乐观锁 |
---|---|---|
核心思想 | "悲观地"认为每次访问数据时,别人大概率会修改它。 所以,在操作数据之前,必须先拿到锁,确保数据在自己操作期间不会被别人修改。 | "乐观地"认为每次访问数据时,别人不太会修改它。 所以,不会先去加锁,而是在更新的时候才去判断在此期间有没有别人更新过这个数据。 |
比喻 | 像写日记。你写的时候,一定会把日记本锁在抽屉里,写完再打开。因为你假设只要你不锁,别人就肯定会来看、来改。 | 像在GitHub上提交代码。你直接从仓库拉取代码,修改,然后git push 。如果这段时间内别人也提交了修改(冲突),系统会告诉你,让你先解决冲突再提交。 |
实现方式 | 传统的互斥锁,如 synchronized 、ReentrantLock 、数据库的 select ... for update 。 | 版本号机制 或 CAS(Compare and Swap)算法。 |
适用场景 | 写多读少,冲突频繁发生的场景。在这种情况下,使用悲观锁可以避免大量的重试开销。 | 读多写少,冲突很少发生的场景。在这种情况下,不加锁可以大大提高吞吐量。 |
二、工作原理详解
1. 悲观锁
悲观锁是一种**“先取锁,后访问”** 的保守策略。它假定并发冲突是常态,因此通过加锁来确保操作的独占性。
工作流程:
- 在任何对数据的操作(读或写)开始前,先获取对应的锁。
- 获取锁成功后,才能对数据进行操作。在此期间,其他所有需要该锁的线程都会被阻塞(挂起)。
- 操作完成后,释放锁。被阻塞的线程有机会获取锁并继续执行。
Java 示例(使用 synchronized
):
public class PessimisticLockExample {private int sharedValue = 0;// synchronized 关键字实现悲观锁public synchronized void increment() {sharedValue++; // 这个操作是原子的,线程安全的}
}
数据库示例(使用 SELECT ... FOR UPDATE
):
-- 开始事务
BEGIN;
-- 1. 查询并锁定这条记录,其他事务无法修改或加锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 2. 计算新余额
-- 3. 更新余额
UPDATE accounts SET balance = 100 WHERE id = 1;
-- 提交事务,释放锁
COMMIT;
2. 乐观锁
乐观锁是一种**“先访问,后冲突检测”** 的开放策略。它假定并发冲突是例外,因此不加锁,而是在更新时验证数据是否被其他线程修改过。
工作流程:
- 读取数据,并记录一个版本号(或数据本身的值)。
- 修改数据。
- 在更新数据回存储时,检查此时数据的版本号是否与第一步读取到的版本号一致。
- 如果一致:说明没有冲突,执行更新,通常也会更新版本号。
- 如果不一致:说明数据已被其他线程修改,本次更新失败。处理策略通常是报错或重试。
实现方式:
-
版本号机制:
在数据表中增加一个version
字段。-- 1. 读取数据,同时获取版本号 (假设 version=1) SELECT balance, version FROM accounts WHERE id = 1;-- 2. 计算新余额 (newBalance)-- 3. 更新时检查版本号 UPDATE accounts SET balance = {newBalance}, version = version + 1 WHERE id = 1 AND version = 1; -- 关键在这里-- 4. 检查更新影响的行数,如果是0,说明版本号不对,更新失败。
-
CAS 算法:
这是乐观锁最常用的技术实现,我们在上一个问题中已经详细讨论过。
Java 示例(使用AtomicInteger
):public class OptimisticLockExample {private AtomicInteger atomicValue = new AtomicInteger(0);public void increment() {int oldValue, newValue;do {// 1. 获取当前值(预期原值)oldValue = atomicValue.get();// 2. 计算新值newValue = oldValue + 1;// 3. 尝试CAS更新:如果当前值还是oldValue,就更新为newValue// 如果不是,则循环重试} while (!atomicValue.compareAndSet(oldValue, newValue));} }
三、优缺点对比
方面 | 悲观锁 | 乐观锁 |
---|---|---|
性能 | 低开销(无冲突时):加锁、释放锁本身开销小。 高开销(有冲突时):线程挂起和唤醒的上下文切换开销巨大。 | 高开销(无冲突时):几乎没有额外开销,性能极高。 低开销(有冲突时):CAS自旋会消耗CPU,但通常比线程切换开销小。 |
死锁 | 可能发生。需要小心处理锁的获取顺序。 | 不会发生。因为根本不加锁。 |
适用场景 | 写操作多、冲突频繁的场景。能保证成功率,避免无谓的重试。 | 读操作多、冲突很少的场景。能极大提升系统吞吐量。 |
数据一致性 | 强一致性。在锁持有期间,数据绝对安全。 | 最终一致性。在提交前可能读到旧数据,但能保证更新操作的原子性和正确性。 |
实现复杂度 | 简单直观。 | 相对复杂,需要处理更新失败的情况(重试逻辑或错误处理)。 |
四、如何选择?
选择乐观锁还是悲观锁,主要取决于应用程序的读写比例和数据竞争激烈程度。
-
强烈推荐使用 乐观锁:
- 读多写少的绝大部分场景。例如:新闻网站的文章浏览量统计、社交媒体的点赞数等。
- 冲突概率很低,你希望系统能有最大的吞吐量。
-
考虑使用 悲观锁:
- 写多读少,且并发写入操作冲突概率很高的场景。例如:秒杀系统中库存的最后几次扣减、银行账户的转账交易。
- 如果重试的代价非常高(例如,业务逻辑非常复杂,重算一次成本很大),不如一开始就加锁。
- 需要与数据库事务紧密集成,并且需要严格保证数据在事务期间不被修改。
总结:
- 悲观锁是 “防患于未然”,通过牺牲性能来保证安全性。
- 乐观锁是 “亡羊补牢”,通过冲突检测来提升性能,在低冲突率下效果极佳。
在现代高并发应用中,由于大部分场景都是读远多于写,所以乐观锁以及基于其思想的无锁编程变得越来越流行。Java并发包 (java.util.concurrent
) 中的很多组件,如 ConcurrentHashMap
,都大量使用了CAS等乐观锁技术。
3️⃣ 参考 3
乐观锁 与 悲观锁 详解
基本概念
悲观锁 (Pessimistic Locking)
思想:假设并发冲突经常发生,因此在访问数据前先加锁,确保数据在操作过程中不会被其他线程修改。
比喻:就像"先上锁,再做事",认为别人很可能来抢资源。
乐观锁 (Optimistic Locking)
思想:假设并发冲突很少发生,因此先直接操作数据,在提交时检查是否有冲突,如果有则重试或报错。
比喻:就像"先做事,再检查",相信别人一般不会来干扰。
工作原理对比
悲观锁工作流程
1. 获取锁
2. 操作数据
3. 提交事务
4. 释放锁
乐观锁工作流程
1. 读取数据(包含版本号)
2. 修改数据
3. 检查版本号是否变化- 如果没变化:提交更新,版本号+1- 如果变化:回滚并重试/报错
实现方式
悲观锁实现
数据库级别
-- MySQL 使用 FOR UPDATE
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;-- SQL Server 使用 WITH (UPDLOCK)
BEGIN TRAN;
SELECT * FROM accounts WITH (UPDLOCK) WHERE id = 1;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
Java 同步机制
// synchronized 关键字
public synchronized void transfer(int from, int to, int amount) {// 转账操作
}// ReentrantLock
private final ReentrantLock lock = new ReentrantLock();public void transfer(int from, int to, int amount) {lock.lock();try {// 转账操作} finally {lock.unlock();}
}
乐观锁实现
版本号机制
-- 数据库表设计
CREATE TABLE products (id BIGINT PRIMARY KEY,name VARCHAR(100),stock INT,version INT DEFAULT 0 -- 版本号字段
);-- 更新操作
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1; -- 检查版本号
CAS 操作
// Java Atomic 类
private AtomicInteger balance = new AtomicInteger(1000);public boolean withdraw(int amount) {int current;int newBalance;do {current = balance.get();if (current < amount) {return false; // 余额不足}newBalance = current - amount;} while (!balance.compareAndSet(current, newBalance));return true;
}
典型应用场景
悲观锁适用场景
- 写操作频繁:并发写入较多的场景
- 临界区复杂:操作需要多个步骤,不能被打断
- 数据一致性要求高:不允许出现任何冲突
例子:
- 银行转账系统
- 库存秒杀(如果竞争激烈)
- 订单支付
乐观锁适用场景
- 读多写少:大部分操作是读取,写入较少
- 冲突概率低:并发写入冲突的可能性较小
- 系统吞吐量要求高:希望避免锁的开销
例子:
- 用户信息更新
- 文章点赞数统计
- 购物车商品数量修改
优缺点对比
悲观锁优缺点
优点:
- 保证强一致性
- 实现简单直观
- 不会出现重试开销
缺点:
- 性能开销大(加锁、释放锁)
- 容易产生死锁
- 降低系统吞吐量
- 不适用于高并发读场景
乐观锁优缺点
优点:
- 性能高(无锁竞争)
- 不会产生死锁
- 系统吞吐量高
- 适用于高并发读场景
缺点:
- 实现相对复杂
- 冲突时需要重试或处理失败
- ABA 问题(可通过版本号解决)
- 不适用于高冲突场景
具体实现示例
电商库存管理
悲观锁实现
@Service
public class InventoryServicePessimistic {@Transactionalpublic boolean deductStock(Long productId, Integer quantity) {// 悲观锁:查询时加锁Product product = productRepository.findByIdWithLock(productId);if (product.getStock() < quantity) {return false;}product.setStock(product.getStock() - quantity);productRepository.save(product);return true;}
}
乐观锁实现
@Service
public class InventoryServiceOptimistic {public boolean deductStock(Long productId, Integer quantity) {int retryCount = 0;while (retryCount < 3) { // 重试机制Product product = productRepository.findById(productId);if (product.getStock() < quantity) {return false;}int rows = productRepository.updateStockWithVersion(productId, quantity, product.getVersion());if (rows > 0) {return true; // 更新成功}retryCount++;}return false; // 重试多次后失败}
}
数据库 Repository 实现
public interface ProductRepository extends JpaRepository<Product, Long> {// 悲观锁查询@Query("SELECT p FROM Product p WHERE p.id = :id")@Lock(LockModeType.PESSIMISTIC_WRITE)Product findByIdWithLock(@Param("id") Long id);// 乐观锁更新@Modifying@Query("UPDATE Product p SET p.stock = p.stock - :quantity, p.version = p.version + 1 " +"WHERE p.id = :id AND p.version = :version")int updateStockWithVersion(@Param("id") Long id, @Param("quantity") Integer quantity, @Param("version") Integer version);
}
性能对比分析
测试场景
// 性能测试对比
@SpringBootTest
class LockPerformanceTest {@Testvoid testPerformance() {// 低并发场景:乐观锁性能更好// 高并发写场景:悲观锁可能更稳定// 高并发读场景:乐观锁明显优势}
}
性能特点
- 悲观锁:随着并发数增加,性能下降明显
- 乐观锁:在低冲突下性能稳定,高冲突时重试开销增大
选择策略
如何选择锁机制?
考虑因素 | 选择悲观锁 | 选择乐观锁 |
---|---|---|
读写比例 | 写多读少 | 读多写少 |
冲突频率 | 冲突频繁 | 冲突较少 |
数据量 | 小数据量 | 大数据量 |
响应要求 | 实时性要求高 | 吞吐量要求高 |
业务复杂度 | 复杂事务 | 简单操作 |
实际建议
- 先分析业务场景:统计读写比例、冲突概率
- 进行压力测试:在不同并发下测试两种方案
- 考虑混合使用:不同业务模块使用不同策略
- 监控和调整:根据线上表现优化策略
总结
乐观锁和悲观锁是解决并发问题的两种重要思路,各有适用场景。在实际开发中,应该根据具体的业务需求、数据特性和性能要求来选择合适的锁机制,有时候甚至需要结合使用来达到最佳效果。
4️⃣ 参考 4
核心思想对比
特性 | 悲观锁 | 乐观锁 |
---|---|---|
哲学观 | “总是最坏的情况” 假设每次数据访问都会导致冲突 | “相信世界是美好的” 假设数据访问通常不会冲突 |
工作方式 | 先取锁,再访问 操作前先锁定数据,确保独占 | 先访问,后检查 直接操作数据,提交时检查是否冲突 |
类比 | 保守的图书管理员 “你要借这本书?先登记,还回来之前别人都不能借。” | 开放的共享文档 “大家随便编辑,保存时系统会检查有没有冲突。” |
1. 悲观锁
工作原理
悲观锁认为数据在并发访问时极有可能发生冲突,因此在访问数据之前必须先获取锁,确保在操作期间其他线程无法修改数据。
实现方式
- 数据库:
SELECT ... FOR UPDATE
- Java:
synchronized
关键字、ReentrantLock
等 - 其他:各种互斥锁(Mutex)、读写锁等
代码示例
Java synchronized:
public class PessimisticCounter {private int count = 0;// 悲观锁:每次只允许一个线程进入public synchronized void increment() {count++; // 这个操作是原子的,其他线程必须等待}public int getCount() {return count;}
}
数据库示例:
-- 开始事务
BEGIN TRANSACTION;-- 悲观锁:锁定这条记录,其他事务无法修改
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;-- 执行更新操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;-- 提交事务,释放锁
COMMIT;
优缺点
优点:
- 简单直接,易于理解
- 保证强一致性,不会出现数据冲突
- 适用于写操作频繁的场景
缺点:
- 性能开销大:加锁、释放锁需要成本
- 并发度低:一个线程持有锁时,其他线程必须等待
- 可能引发死锁
- 降低系统吞吐量
2. 乐观锁
工作原理
乐观锁假设数据访问很少发生冲突,因此允许多个线程同时读取和修改数据,只在提交时检查数据是否被其他线程修改过。
实现方式
- 版本号机制:为数据添加版本号字段
- CAS操作:比较并交换
- 时间戳:使用时间戳判断数据新旧
代码示例
数据库版本号实现:
-- 为表添加版本号字段
ALTER TABLE products ADD version INT DEFAULT 0;-- 乐观锁更新:只有版本号匹配时才更新
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 123 AND version = 5; -- 读取时的版本号是5-- 检查影响行数:如果是0,说明版本号不匹配,需要重试
Java Atomic类(基于CAS):
public class OptimisticCounter {private AtomicInteger count = new AtomicInteger(0);// 乐观锁:使用CAS,无需阻塞其他线程public void increment() {int oldValue, newValue;do {oldValue = count.get(); // 读取当前值newValue = oldValue + 1; // 计算新值} while (!count.compareAndSet(oldValue, newValue)); // CAS操作,失败则重试}public int getCount() {return count.get();}
}
更简洁的写法:
public class OptimisticCounter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 内部已经实现了CAS重试逻辑}
}
优缺点
优点:
- 高并发性能:读操作不会被阻塞
- 避免死锁:无锁机制
- 高吞吐量:适合读多写少的场景
缺点:
- ABA问题:值从A→B→A,CAS无法感知中间变化
- 自旋开销:高竞争环境下重试消耗CPU
- 实现复杂:需要处理更新失败的情况
3. 详细对比表格
对比维度 | 悲观锁 | 乐观锁 |
---|---|---|
并发性能 | 低 | 高 |
开销成本 | 加锁、上下文切换开销大 | 冲突检测开销,可能有重试开销 |
适用场景 | 写操作多、冲突频繁 | 读操作多、冲突较少 |
数据一致性 | 强一致性 | 最终一致性 |
实现复杂度 | 简单 | 相对复杂 |
典型应用 | 银行转账、库存扣减(高竞争) | 计数器、点赞、评论系统 |
风险 | 死锁、性能瓶颈 | ABA问题、更新失败 |
4. 如何选择?
选择悲观锁的场景:
- 数据竞争激烈,冲突概率很高
- 关键业务数据,必须保证强一致性
- 写操作远多于读操作
- 短期事务,锁持有时间很短
例子: 电商库存秒杀、银行账户转账
选择乐观锁的场景:
- 读多写少,冲突概率较低
- 系统性能要求高,需要高吞吐量
- 锁竞争不激烈
- 可以接受重试机制
例子: 文章点赞数统计、用户信息更新、文档协同编辑
5. 实际应用中的最佳实践
乐观锁的重试策略:
public boolean updateWithRetry(Product product, int newStock) {int retries = 3; // 最大重试次数while (retries > 0) {int currentVersion = product.getVersion();// 尝试更新boolean success = productDao.updateStock(product.getId(), newStock, currentVersion);if (success) {return true;}// 更新失败,重新加载数据product = productDao.getById(product.getId());retries--;}return false; // 重试多次后仍然失败
}
解决ABA问题:
使用带版本号的乐观锁:
// 使用AtomicStampedReference解决ABA问题
AtomicStampedReference<Integer> atomicValue = new AtomicStampedReference<>(0, 0); // 初始值+版本戳public void updateValue() {int[] stampHolder = new int[1];int oldValue, newValue;do {oldValue = atomicValue.get(stampHolder); // 获取值和版本戳newValue = oldValue + 1;} while (!atomicValue.compareAndSet(oldValue, newValue, stampHolder[0], stampHolder[0] + 1));
}
总结
- 悲观锁:先锁定,再操作。“宁可错杀一千,不可放过一个”
- 乐观锁:先操作,后检查。“相信世界是美好的,但留个心眼”
在现代高并发系统中,乐观锁往往更受欢迎,因为它能提供更好的扩展性和性能。但在高竞争的业务场景中,悲观锁仍然是必要的选择。
理解两者的区别和适用场景,能够帮助我们在系统设计中做出更合理的技术选型。
5️⃣ 参考 5
好的,我们来详细探讨 乐观锁 和 悲观锁 这两种经典的并发控制策略。
它们代表了解决并发问题的两种截然不同的哲学思想。理解它们的区别是设计高效、正确并发程序的关键。
核心思想比喻
首先,用一个生动的比喻来理解它们:
-
悲观锁:像是一个悲观的管理员。
- 想法:“这个世界太危险了,只要有人用这个东西,就很可能会和别人冲突。”
- 行为:所以,每当一个人想使用某个资源时,管理员都会先把资源锁起来,交给这个人。其他人想用必须等待,直到这个人用完后把锁打开。这确保了在锁住期间,绝对没有其他人能干扰。
- 例子:数据库的
SELECT ... FOR UPDATE
,Java 的synchronized
关键字。
-
乐观锁:像是一个乐观的管理员。
- 想法:“这个世界很和谐,通常不会有人跟我抢同一个东西,冲突是小概率事件。”
- 行为:所以,管理员不会一开始就加锁。任何人都可以直接拿到资源去修改。但是,在提交修改时,管理员会检查一下:“在我给你资源之后,有没有其他人动过它?”
- 如果没人动过(版本号/值和之前一致),就允许提交。
- 如果有人动过(版本号/值已变化),就告知操作失败,让你重新读取最新值并再次尝试。
- 例子:CAS 操作,数据库的
version
字段,Git 的合并操作。
详细对比
下表清晰地展示了两者的核心差异:
特性 | 悲观锁 | 乐观锁 |
---|---|---|
基本思想 | 认为数据在并发访问时一定会发生冲突,因此采取先加锁再访问的策略。 | 认为数据在并发访问时大概率不会冲突,只在更新时检测是否冲突。 |
实现方式 | 传统的锁机制,如 synchronized 、ReentrantLock 、数据库行锁/表锁。 | 版本号机制 或 CAS(Compare and Swap) 算法。 |
工作流程 | 1. 获取锁 2. 读写数据 3. 释放锁 | 1. 读取数据及版本号 2. 修改数据 3. 写入时验证版本号/CAS 4. 若失败则重试或报错 |
线程状态 | 获取锁失败的线程会被挂起(阻塞),进入等待队列,发生线程上下文切换。 | 检测到冲突(CAS失败)的线程不会阻塞,而是通过自旋(循环重试) 再次尝试。 |
开销来源 | 线程的阻塞、唤醒、上下文切换带来的性能开销。 | 自旋重试 对 CPU 资源的消耗。 |
适用场景 | 写多读少、冲突频繁、临界区代码执行时间长的场景。 | 读多写少、冲突较少、临界区代码执行快的场景。 |
主要优势 | 简单粗暴,能保证强一致性,不会出现重试的活锁问题。 | 无锁编程,性能高,避免了线程阻塞的开销,天生免疫死锁。 |
主要劣势 | 性能低,可能存在死锁风险。 | ABA 问题,在竞争激烈时自旋会浪费 CPU。 |
技术实现举例
1. 悲观锁实现
Java synchronized
关键字
public class PessimisticCounter {private int count = 0;// 使用synchronized确保同一时刻只有一个线程能执行此方法public synchronized void increment() {count++; // 从读取到写入,整个过程都被锁保护}
}
在这个例子中,一个线程进入 increment()
方法时自动获取锁,其他线程必须等待它执行完毕释放锁后才能进入。
数据库 SELECT ... FOR UPDATE
BEGIN TRANSACTION;
-- 悲观锁:锁定这条记录,其他事务无法修改
SELECT value FROM inventory WHERE product_id = 123 FOR UPDATE;
-- ... 进行一些业务逻辑计算
UPDATE inventory SET value = new_value WHERE product_id = 123;
COMMIT;
2. 乐观锁实现
Java AtomicInteger
(基于 CAS)
public class OptimisticCounter {private AtomicInteger count = new AtomicInteger(0);public void increment() {int oldValue;int newValue;do {oldValue = count.get(); // 1. 读取原值newValue = oldValue + 1; // 2. 计算新值// 3. CAS: 如果当前值还是oldValue,就更新为newValue// 如果不是,说明被其他线程改过,则循环重试} while (!count.compareAndSet(oldValue, newValue));}
}
数据库版本号机制
-- 假设表中有个version字段
SELECT value, version FROM inventory WHERE product_id = 123;
-- ... 在应用层计算新value,并 version = version + 1UPDATE inventory
SET value = new_value, version = version + 1
WHERE product_id = 123 AND version = old_version;
-- 如果这条SQL影响的行数为0,说明version被其他事务修改过了,更新失败
如何选择?
选择乐观锁还是悲观锁,取决于你的具体应用场景:
-
强烈推荐悲观锁:
- 临界区代码复杂或执行时间长:如果检查数据到提交更新之间的操作很耗时,用乐观锁会导致大量线程长时间自旋,CPU 开销巨大。不如让其他线程阻塞等待。
- 写操作非常频繁,冲突概率极高:在这种情况下,乐观锁会陷入不断重试的“活锁”状态,大部分 CPU 周期都浪费在失败的重试上,性能反而远低于悲观锁。
-
强烈推荐乐观锁:
- 读多写少:这是乐观锁的理想场景,冲突很少发生,大部分操作都不需要加锁,性能极高。
- 临界区代码简单、执行速度快:即使发生冲突,重试的代价也很小。
- 需要避免死锁:乐观锁机制天生不会产生死锁。
总结
锁类型 | 哲学 | 好比 | 最佳场景 |
---|---|---|---|
悲观锁 | “先下手为强” | 独占的卫生间(锁门) | 写多读少,强竞争,长事务 |
乐观锁 | “君子动口不动手” | 乐观的版本管理员 | 读多写少,弱竞争,短事务 |
在现代高并发应用中,由于读请求远多于写请求的普遍性,乐观锁(以及其思想延伸出的无锁数据结构) 的使用越来越广泛,成为了实现高性能系统的关键技术之一。
6️⃣ 参考 6
乐观锁和悲观锁是并发控制中两种核心策略,用于解决多线程/多进程环境下的数据一致性问题。它们的核心区别在于对并发冲突的预期和处理方式。以下是详细对比:
1. 悲观锁(Pessimistic Locking)
核心思想
- 假设冲突频繁发生,因此在访问共享资源前先加锁,确保独占访问。
- 类似于“先关门再操作”,防止其他线程干扰。
实现方式
- 数据库层面:
SELECT ... FOR UPDATE
(行锁)、TABLE LOCK
(表锁)。 - 编程语言:
synchronized
(Java)、mutex
(C++)、Lock
接口(如ReentrantLock
)。
特点
- 阻塞式:线程获取不到锁时会等待(可能死锁)。
- 开销大:锁的获取/释放、上下文切换消耗资源。
- 适用场景:
- 冲突概率高(如写多读少)。
- 需要强一致性(如金融交易)。
示例(Java)
// 使用synchronized(悲观锁)
public synchronized void updateData() {// 临界区代码
}// 使用ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {// 临界区代码
} finally {lock.unlock();
}
2. 乐观锁(Optimistic Locking)
核心思想
- 假设冲突很少发生,因此不加锁直接操作,但在提交时检查数据是否被修改。
- 类似于“先操作再验证”,通过版本号或时间戳检测冲突。
实现方式
- 版本号机制:数据表中增加
version
字段,更新时校验版本。-- 更新时检查版本 UPDATE table SET value = new_value, version = version + 1 WHERE id = 1 AND version = old_version;
- CAS(Compare-And-Swap):硬件级原子操作(如
AtomicInteger
)。 - 时间戳:类似版本号,但使用时间戳标记修改。
特点
- 非阻塞式:冲突时重试或回滚,不阻塞线程。
- 开销小:无锁的获取/释放,但冲突时可能多次重试。
- 适用场景:
- 冲突概率低(如读多写少)。
- 高并发、低延迟要求(如Web服务)。
示例(Java CAS)
import java.util.concurrent.atomic.AtomicInteger;AtomicInteger counter = new AtomicInteger(0);public void increment() {int oldValue;int newValue;do {oldValue = counter.get();newValue = oldValue + 1;} while (!counter.compareAndSet(oldValue, newValue)); // CAS重试
}
示例(数据库版本号)
// 伪代码:乐观锁更新
User user = getUserFromDB(1); // 假设version=1
user.setName("New Name");
int rowsAffected = updateUser(user); // 实际SQL会检查version
if (rowsAffected == 0) {throw new OptimisticLockException("数据已被其他线程修改");
}
3. 对比总结
特性 | 悲观锁 | 乐观锁 |
---|---|---|
冲突预期 | 频繁发生 | 很少发生 |
加锁时机 | 操作前加锁 | 操作后验证 |
并发性能 | 低(阻塞) | 高(非阻塞) |
实现复杂度 | 低(直接加锁) | 高(需处理冲突重试) |
适用场景 | 写多读少、强一致性 | 读多写少、高并发 |
典型问题 | 死锁、锁竞争 | ABA问题、重试风暴 |
4. 如何选择?
- 用悲观锁:
- 数据竞争激烈(如秒杀系统库存扣减)。
- 操作耗时短(避免长时间持有锁)。
- 用乐观锁:
- 数据冲突概率低(如评论点赞)。
- 需要高吞吐量(如分布式缓存更新)。
5. 混合使用案例
- 数据库事务:结合悲观锁(行锁)和乐观锁(版本号)。
BEGIN TRANSACTION; SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- 悲观锁 -- 业务逻辑... UPDATE orders SET status = 'SHIPPED', version = version + 1 WHERE id = 1 AND version = 10; -- 乐观锁验证 COMMIT;
- 分布式系统:CAS用于无锁数据结构,悲观锁用于分布式事务。
6. 现代框架中的实践
- Spring事务:默认使用数据库悲观锁(通过
@Transactional
)。 - JPA/Hibernate:支持
@Version
注解实现乐观锁。 - Redis分布式锁:基于
SETNX
的悲观锁变种。 - ZooKeeper:通过临时节点实现分布式悲观锁。
理解乐观锁和悲观锁的本质,能帮助你在设计高并发系统时做出更合理的同步策略选择。
7️⃣ 参考 7
乐观锁与悲观锁
基本概念
乐观锁(Optimistic Locking)
- 定义:乐观锁认为在操作数据时,不会有其他线程或进程同时修改数据,因此不会在读取时加锁,而是在提交数据时检查是否有其他线程修改了数据。
- 核心思想:“我相信不会有人同时修改数据,所以我不上锁,但更新时会检查是否有人修改过”。
- 实现方式:
- 版本号机制:在数据中增加一个版本号字段,更新时检查版本号是否与预期一致。
- CAS(Compare and Swap):通过原子操作比较并交换,只有在预期值与实际值相同时才更新。
悲观锁(Pessimistic Locking)
- 定义:悲观锁认为在操作数据时,其他线程或进程很可能会修改数据,因此在读取或操作数据时会先加锁。
- 核心思想:“我担心有人会同时修改数据,所以我要先上锁,确保我操作时不会被干扰”。
- 实现方式:
- 数据库中的行锁、表锁
- Java中的synchronized关键字
- 传统的互斥锁
主要区别
特性 | 乐观锁 | 悲观锁 |
---|---|---|
思想 | 乐观地认为不会发生冲突 | 悲观地认为会发生冲突 |
加锁时机 | 读时不加锁,更新时检查 | 读时就加锁 |
锁的持有时间 | 短(仅在更新时) | 长(从读到写完成) |
适用场景 | 多读少写(如查询系统) | 多写少读(如交易系统) |
并发性能 | 高并发下性能更好 | 高并发下性能较差 |
实现复杂度 | 较高(需处理重试、ABA问题) | 较低(直接使用锁机制) |
典型实现 | CAS、版本号机制 | synchronized、数据库行锁 |
适用场景
乐观锁适用场景
- 读操作远多于写操作的系统(如新闻网站、博客系统)
- 数据争用不激烈,偶尔回滚事务的成本低于读取数据时锁定数据的成本
- 例如:在数据库中使用版本号机制进行更新
悲观锁适用场景
- 写操作远多于读操作的系统(如银行交易系统、订单系统)
- 数据争用激烈,发生并发冲突时用锁保护数据的成本低于回滚事务的成本
- 例如:使用synchronized关键字保护共享资源,或数据库的行锁
乐观锁的实现示例(CAS)
// Java中使用CAS实现的原子自增
AtomicInteger atomicInt = new AtomicInteger(0);
int newValue;
int current;
do {current = atomicInt.get(); // 读取当前值(预期原值)newValue = current + 1; // 计算新值
} while (!atomicInt.compareAndSet(current, newValue)); // CAS操作
悲观锁的实现示例(synchronized)
// Java中使用synchronized实现的线程安全方法
public synchronized void updateData() {// 读取数据// 修改数据// 保存数据
}
乐观锁的缺点与解决方案
-
ABA问题:
- 问题:线程1读取值A,线程2将A改为B,又改回A,线程1认为值未变,执行CAS成功。
- 解决方案:使用带版本号的CAS(如Java的AtomicStampedReference)。
-
自旋开销:
- 问题:高竞争情况下,CAS失败后不断重试,导致CPU资源浪费。
- 解决方案:限制重试次数,或在重试前加入短暂休眠。
总结
- 乐观锁:适用于"多读少写"场景,性能高,但需要处理重试和ABA问题。
- 悲观锁:适用于"多写少读"场景,实现简单,但会阻塞其他线程,影响并发性能。
正如知识库[1]中所说:“乐观锁适用于多读少写的情况,可以提高并发性能;悲观锁适用于多写少读的情况,可以保证数据的一致性,但是会影响并发性能。”
在实际应用中,选择哪种锁取决于具体业务场景和数据访问模式。现代并发框架(如Java的java.util.concurrent包)通常提供多种并发控制机制,可以根据需要灵活选择。
8️⃣ 参考 8
乐观锁和悲观锁是并发编程中解决资源竞争的两种核心策略,它们基于对“并发冲突概率”的不同假设,衍生出完全不同的实现思路。理解这两种锁的设计理念,能帮你在实际开发中选择更合适的并发控制方案。
一、核心思想:对冲突的“假设”不同
-
悲观锁:假设“并发冲突一定会发生”。
因此,在操作共享资源前,会先通过加锁“独占”资源,确保同一时间只有自己能操作,其他线程必须等待锁释放后才能尝试。
核心逻辑:“先上锁,再操作,不信任任何并发”。 -
乐观锁:假设“并发冲突很少发生”。
因此,操作时不会先加锁,而是直接修改,仅在“提交更新”时检查资源是否被其他线程修改过。如果未被修改,则更新成功;如果已被修改,则放弃更新并重试(或返回失败)。
核心逻辑:“先操作,后检查,信任大多数情况无冲突”。
二、实现方式与典型案例
1. 悲观锁的实现与案例
悲观锁的核心是“通过锁机制强制独占资源”,实现依赖底层的锁管理(如操作系统的互斥锁、数据库的锁机制)。
典型案例:
- Java中的
synchronized
关键字:进入同步代码块前会获取对象锁,其他线程需阻塞等待。 - Java的
ReentrantLock
:显式加锁(lock()
)和解锁(unlock()
),未获取锁的线程会阻塞。 - 数据库的行锁/表锁:例如MySQL的
InnoDB
在执行update
时会自动加行锁,其他事务需等待锁释放才能修改同一行。 - 文件锁:多线程读写同一文件时,通过操作系统的文件锁确保同一时间只有一个线程能写入。
2. 乐观锁的实现与案例
乐观锁的核心是“冲突检测”,实现依赖“版本标识”或“原子操作”,无需显式加锁。
常见实现方式:
-
版本号机制:为数据增加一个“版本字段”(如
version
),每次更新时:- 读取数据时同时获取当前版本号(如
version=1
); - 更新时检查版本号是否仍为
1
:若一致,更新数据并将版本号+1(如version=2
);若不一致,说明数据已被修改,更新失败。
例:数据库中update table set value=xxx, version=version+1 where id=1 and version=1
。
- 读取数据时同时获取当前版本号(如
-
CAS操作:通过硬件原子指令(如CPU的
cmpxchg
)实现“比较-交换”的原子性,本质是乐观锁的一种底层实现。
例:Java的AtomicInteger
通过CAS实现getAndIncrement()
(原子自增),无需加锁,冲突时自动重试。
其他案例:
- 并发容器
ConcurrentHashMap
(JDK 1.8+):读操作无锁,写操作通过CAS+局部锁实现,偏向乐观锁思路。 - Redis的
watch
命令:监控键值,提交事务时检查键是否被修改,未修改则执行事务,否则事务失败。
三、优缺点对比
维度 | 悲观锁 | 乐观锁 |
---|---|---|
并发性能 | 差:锁竞争会导致线程阻塞、上下文切换,高并发下效率低。 | 好:无锁阻塞,仅冲突时重试,低冲突场景下性能优势明显。 |
适用场景 | 写操作频繁、冲突概率高(如秒杀系统的库存扣减)。 | 读操作频繁、冲突概率低(如商品详情页的浏览计数)。 |
实现复杂度 | 简单:依赖现成的锁机制,无需手动处理冲突。 | 复杂:需手动实现版本控制或重试逻辑,还要处理重试失败(如避免无限循环)。 |
资源消耗 | 锁管理(如操作系统内核态切换)的开销大。 | 冲突少时几乎无额外开销;冲突频繁时,重试会消耗CPU。 |
阻塞风险 | 有:未获取锁的线程会被阻塞。 | 无:失败线程可立即重试或退出,不阻塞其他线程。 |
四、关键区别总结
- 核心假设:悲观锁假设“冲突必然发生”,乐观锁假设“冲突很少发生”。
- 操作流程:悲观锁“先锁后操作”,乐观锁“先操作后检查”。
- 阻塞性:悲观锁是阻塞式的(未获取锁的线程会等待),乐观锁是非阻塞式的(失败线程可立即重试)。
五、如何选择?
- 若场景中写操作多、冲突频繁(如抢票、库存扣减),用悲观锁更稳妥,避免乐观锁的大量重试开销。
- 若场景中读操作多、冲突少(如数据查询、计数器),用乐观锁性能更优,减少锁竞争的损耗。
例如:电商商品详情页的“浏览量+1”适合乐观锁(CAS),而订单支付的“库存扣减”适合悲观锁(数据库行锁)。
9️⃣ 参考 9
下面把“乐观锁”与“悲观锁”放在同一张对照表上,从思想、实现、代价、典型场景到代码/命令示例一次性讲清。看完就能判断什么时候该“乐观”、什么时候必须“悲观”。
1. 一句话区分
类型 | 核心思想 |
---|---|
悲观锁(Pessimistic Lock) | 总觉得别人会跟我抢,先加锁再操作,用完再释放。 |
乐观锁(Optimistic Lock) | 相信冲突概率低,直接操作,提交时再检查有没有人改过,改过就重试或放弃。 |
2. 对照全表
维度 | 悲观锁 | 乐观锁 |
---|---|---|
同步时机 | 提前加锁 | 延后验证(提交阶段) |
阻塞/自旋 | 会阻塞或排队 | 不会阻塞,失败重试 |
CPU 消耗 | 低(让出 CPU 等待) | 高(重试循环) |
死锁可能 | 有(多锁顺序不当) | 无(无锁或一次性获取) |
粒度 | 可大可小(表、页、行、对象) | 通常细粒度(行、对象) |
**典型实现 | 1. 数据库 SELECT ... FOR UPDATE / LOCK IN SHARE MODE 2. synchronized / ReentrantLock | 1. 版本号 / 时间戳字段 2. CAS( AtomicInteger 、AtomicReference )3. MVCC 快照 |
适用场景 | 写并发高、冲突概率高、临界区耗时较长 | 读多写少、冲突概率低、临界区极短 |
失败策略 | 排队等待 | 重试 / 回滚 / 抛异常 |
语言级例子 | synchronized(this){ ... } | atomic.incrementAndGet() |
数据库例子 | SELECT * FROM account WHERE id=1 FOR UPDATE; | UPDATE account SET balance=900, version=version+1 WHERE id=1 AND version=5; |
3. 数据库层面直观对比
悲观锁(MySQL InnoDB)
BEGIN;
-- 先锁住该行,别的事务试图改同一行时会阻塞
SELECT balance FROM account WHERE id = 1 FOR UPDATE;
-- 业务计算 ...
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
乐观锁(版本号)
-- 1. 先读数据和版本号
SELECT balance, version FROM account WHERE id = 1;-- 2. 业务层计算新余额 newBalance-- 3. 提交时检查版本号是否变动
UPDATE accountSET balance = #{newBalance},version = version + 1WHERE id = 1AND version = #{oldVersion};-- 返回 affected rows == 0 表示被其他事务改过,重试或抛异常
4. Java 代码级对比
悲观锁
private final Object lock = new Object();
void transfer(int amt){synchronized(lock){ // 显式阻塞balance -= amt;}
}
乐观锁(CAS)
AtomicInteger balance = new AtomicInteger(1000);
void transfer(int amt){int oldVal, newVal;do{oldVal = balance.get();newVal = oldVal - amt;}while(!balance.compareAndSet(oldVal, newVal)); // 失败就自旋重试
}
5. 如何选型(实战口诀)
- 写冲突频繁或临界区耗时 → 悲观锁(省得反复重试)。
- 读远多于写,冲突概率低 → 乐观锁(吞吐量高)。
- 对死锁敏感(高可用场景)→ 优先考虑乐观或可重入悲观锁+顺序加锁策略。
- 需要长时间事务(报表、人工审核)→ 必须用悲观锁或快照隔离,否则重试代价爆炸。
6. 常见误区
- “乐观锁一定更快”
→ 写冲突高时,重试开销可能远超阻塞等待。 - “用了乐观锁就不会阻塞”
→ 失败重试的自旋也会烧 CPU,极端情况还不如直接阻塞。 - “MVCC 就是乐观锁”
→ MVCC 提供快照一致性,但提交时仍需版本检查才算乐观锁;它只是实现乐观锁的基础之一。
7. 一张脑图总结(文字版)
冲突概率低 —— 乐观锁 —— CAS / 版本号 —— 读多写少 —— 无阻塞、重试
冲突概率高 —— 悲观锁 —— synchronized / FOR UPDATE —— 写密集 —— 先锁后做
掌握这张表,面试/架构评审时就能秒回:
“读多写少用乐观,写多读少用悲观;长事务、高冲突必须悲观,短临界、低冲突可乐观。”
🔟 参考 10
乐观锁与悲观锁详解
一、基本概念
锁类型 | 核心思想 | 适用场景 | 典型实现 |
---|---|---|---|
悲观锁 | 假设并发冲突必然发生,操作前先加锁 | 写操作频繁的高冲突场景 | synchronized 、ReentrantLock |
乐观锁 | 假设并发冲突概率低,操作后验证数据 | 读多写少的低冲突场景 | CAS、版本号机制 |
数学描述:
设共享资源为 RRR,线程 TiT_iTi 执行操作 OiO_iOi
- 悲观锁:$ \forall T_i, \ O_i \rightarrow \text{lock}® \rightarrow \text{execute} \rightarrow \text{unlock}® $
- 乐观锁:$ O_i \rightarrow \text{verify}(R_{\text{old}}, R_{\text{new}}) \rightarrow \begin{cases}
\text{commit} & \text{if } R_{\text{current}} = R_{\text{old}} \
\text{retry} & \text{otherwise}
\end{cases} $
二、工作原理对比
悲观锁流程:
graph TDA[线程请求资源] --> B[获取锁]B --> C{锁是否可用?}C -- 是 --> D[执行操作]C -- 否 --> E[阻塞等待]D --> F[释放锁]
乐观锁流程:
graph TDA[读取资源当前值 V_old] --> B[执行非原子操作]B --> C[CAS/VERSION 验证]C -- V_current=V_old --> D[提交更新]C -- V_current≠V_old --> E[重试或放弃]
三、特性对比分析
特性 | 悲观锁 | 乐观锁 |
---|---|---|
数据一致性 | 强一致性 | 最终一致性 |
并发性能 | 高冲突场景稳定,但上下文切换开销大 | 低冲突场景吞吐量高,无阻塞等待 |
资源消耗 | 线程阻塞降低CPU利用率 | 自旋重试可能浪费CPU周期 |
死锁风险 | 存在死锁可能(需超时机制) | 无死锁风险 |
实现复杂度 | 简单直观 | 需处理重试逻辑和ABA问题 |
四、典型实现代码
悲观锁示例(Java ReentrantLock):
ReentrantLock lock = new ReentrantLock();
public void updateResource() {lock.lock(); // 获取锁try {// 修改共享资源resource.setValue(newValue);} finally {lock.unlock(); // 必须释放锁}
}
乐观锁示例(Java CAS):
AtomicInteger resource = new AtomicInteger(0);
public void updateResource() {int oldVal;do {oldVal = resource.get(); // 读取当前值int newVal = oldVal + 1; // 计算新值} while (!resource.compareAndSet(oldVal, newVal)); // CAS重试
}
五、应用场景
场景 | 推荐锁类型 | 原因 |
---|---|---|
银行转账操作 | 悲观锁 | 高冲突写操作需强一致性 |
商品库存扣减 | 乐观锁 | 低冲突场景,避免阻塞提高并发能力 |
数据库事务 | 悲观锁 | 通过行锁/表锁保证ACID |
并发计数器 | 乐观锁 | CAS操作轻量高效(如AtomicLong) |
版本控制系统提交 | 乐观锁 | 通过版本号解决冲突(类似Git合并) |
优化提示:
在高冲突场景中,可通过锁分段(如ConcurrentHashMap)或LongAdder优化乐观锁性能,减少CAS自旋开销。
相关问题
- 如何通过版本号机制在数据库中实现乐观锁?
- 为什么Java的
synchronized
在JDK 1.6后性能大幅提升? - 如何解决乐观锁的ABA问题?对比
AtomicStampedReference
与AtomicMarkableReference
- 在分布式系统中实现悲观锁需要考虑哪些额外因素?
- 如何量化评估系统适合悲观锁还是乐观锁?(冲突检测公式 Pconflict=NfailNtotalP_{\text{conflict}} = \frac{N_{\text{fail}}}{N_{\text{total}}}Pconflict=NtotalNfail )
🤔 参考 11
了解乐观锁和悲观锁的区别对编写并发程序很重要。简单来说,它们是处理并发操作的两种不同策略,没有绝对的好坏,关键在于是否适用于特定场景。我先用一个表格帮你快速把握核心区别:
特性维度 | 乐观锁 (Optimistic Locking) | 悲观锁 (Pessimistic Locking) |
---|---|---|
核心思想 | 假设冲突很少发生,操作数据时不上锁,更新时检查数据是否被改动过 | 假设冲突经常发生,操作数据前先加锁,确保独占 |
实现方式 | 版本号/时间戳,CAS (Compare-And-Swap) | synchronized, ReentrantLock, SELECT … FOR UPDATE (数据库) |
并发性能 | 较高(无锁操作,无阻塞) | 较低(加锁释放锁有开销,线程阻塞会引发上下文切换) |
开销 | 冲突较少时较小;冲突严重时,自旋重试可能带来较大CPU开销 | 加锁、释放锁、线程阻塞和唤醒的开销较大 |
适用场景 | 读多写少,低冲突率(通常认为冲突概率低于10%) | 写多读少,高竞争和高冲突率(如金融转账、库存扣减) |
数据一致性 | 最终一致性 | 强一致性 |
典型问题 | ABA问题(可通过AtomicStampedReference 带版本号解决) | 死锁风险 |
🔧 工作原理与实现
两者的工作方式截然不同。
-
乐观锁工作原理:乐观锁假设并发冲突的概率很小,因此在进行数据操作时并不加锁,而是在提交更新时,验证数据在此期间是否被其他线程修改过。常见的实现方式有:
- 版本号/时间戳机制:为数据增加一个版本号字段(如
version
)。读取数据时同时获取版本号,更新时,若当前读取的版本号与数据库中的一致才执行更新,同时版本号增加。-- 更新操作,仅当版本号匹配时执行 UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = 2; -- 检查当前版本是否匹配
- CAS (Compare-And-Swap) 机制:CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当 V 的值等于 A 时,CAS 才会原子性地将 V 的值更新为 B,否则不做任何操作。许多 CAS 操作是自旋的,即如果不成功,会一直重试,直到成功为止。
// Java中AtomicInteger的CAS自增操作底层类似原理 public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset); // 获取当前值} while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS失败则重试return v; }
- 版本号/时间戳机制:为数据增加一个版本号字段(如
-
悲观锁工作原理:悲观锁假设并发冲突的概率很高,因此在访问共享资源之前,会先获取锁来保护资源,确保在持有锁的期间内,其他线程无法修改数据。常见的实现方式有:
- Java中的
synchronized
关键字:synchronized
是 Java 内置的同步机制,它可以修饰方法或代码块,保证在同一时刻最多只有一个线程可以执行该段代码。// 同步代码块 public void increment() {synchronized(lock) { // 对lock对象加锁count++;} // 解锁 }
- Java中的
ReentrantLock
:ReentrantLock
是 Java 并发包 (java.util.concurrent.locks
) 中提供的显式锁实现,它比synchronized
更灵活,提供了尝试非阻塞获取锁、可中断的锁等待、超时获取锁以及公平锁等高级功能。Lock lock = new ReentrantLock(); // 默认非公平锁 public void increment() {lock.lock(); // 获取锁try {count++;} finally {lock.unlock(); // 确保锁被释放} }
- 数据库悲观锁(如
SELECT ... FOR UPDATE
):在数据库中,悲观锁通常通过SELECT ... FOR UPDATE
语句实现。该语句会为选中的行加上排他锁,直到事务提交或回滚才会释放锁,期间其他事务无法修改或对这些记录加锁。START TRANSACTION; -- 对id=1的记录加排他锁 SELECT * FROM products WHERE id = 1 FOR UPDATE; -- ... 执行业务计算和更新 UPDATE products SET stock = stock - 1 WHERE id = 1; COMMIT; -- 提交事务,释放锁
- Java中的
⚖️ 优缺点与挑战
-
乐观锁的优缺点:
- 优点:在读多写少、低竞争的环境中,由于避免了加锁、释放锁的开销和线程阻塞,性能通常更好,吞吐量更高。
- 缺点:
- ABA 问题:如果一个值原来是 A,变成了 B,后来又变回了 A,CAS 操作会误以为它没有被修改过。通常通过引入版本号或时间戳(如
AtomicStampedReference
)来解决。 - 自旋开销:在高竞争环境下,如果线程长时间 CAS 失败不停重试,会空耗 CPU 资源。
- 只能保证一个共享变量的原子操作:CAS 通常只能保证一个共享变量的原子操作。要对多个变量进行原子更新,需使用锁或将多个变量合并到一个对象中,再用
AtomicReference
来引用。
- ABA 问题:如果一个值原来是 A,变成了 B,后来又变回了 A,CAS 操作会误以为它没有被修改过。通常通过引入版本号或时间戳(如
-
悲观锁的优缺点:
- 优点:保证强一致性,实现简单直观,适合临界区执行时间长、冲突频率高、写操作多的场景,能有效避免脏读、不可重复读等问题。
- 缺点:
- 性能开销大:加锁、释放锁以及线程阻塞和唤醒都需要资源,在高并发场景下可能降低系统吞吐量。
- 可能引起死锁:不正确的加锁顺序或时机可能导致死锁。
- 可能降低并发性:一个线程持有锁会导致其它所有需要此锁的线程挂起。
🎯 适用场景
-
优先考虑乐观锁的场景:
- 读多写少:例如商品缓存、配置信息等读远大于写的场景。
- 冲突概率低:例如大多数评论系统、点赞功能,同时更新同一条记录的概率较低。
- 对性能和吞吐量要求高,且能接受重试代价的场景。
-
优先考虑悲观锁的场景:
- 写多读少:例如银行转账、库存扣减(如秒杀)、票务座位锁定等需要保证数据强一致性的高频写场景。
- 临界区代码执行时间长:操作需要较长时间完成,不适合频繁重试。
- 需要严格保证数据一致性,且冲突概率较高的场景。
💡 选型思考提示
选择乐观锁还是悲观锁,可以问自己几个问题:
- 冲突概率:我的应用是读多还是写多?同时更新同一条数据的可能性有多大?
- 性能要求:应用是吞吐量优先还是强一致性优先?
- 临界区大小:竞争资源的代码执行需要多长时间?是否包含慢操作(如IO)?
- 重试成本:如果使用乐观锁,冲突后重试的代价是否可接受?
💎 总结
锁类型 | 核心思想 | 典型应用场景 |
---|---|---|
乐观锁 | 事后检查,冲突则重试或放弃 | 读多写少、低冲突场景(如缓存、评论) |
悲观锁 | 事前加锁,确保独占 | 写多读少、高冲突场景(如转账、库存扣减) |
现代应用程序中,常常会根据不同的业务场景混合使用这两种锁。例如,在电商系统中,商品详情查看(读多)可能采用乐观锁,而库存扣减和余额支付(写多且强一致)则可能采用悲观锁。
希望这些信息能帮助你更好地理解乐观锁和悲观锁。如果你有具体的应用场景,我们可以进一步探讨哪种策略更合适。
🤔 参考 12
乐观锁与悲观锁的核心对比与适用场景分析
一、核心原理与实现方式
-
乐观锁
- 假设:默认数据不会发生冲突,仅在提交时校验是否被修改。
- 实现方式:
- 版本号机制:数据表增加
version
字段,更新时比对版本号是否一致。 - CAS(Compare And Swap):通过硬件指令原子性比较并交换值,如 Java 的
AtomicInteger
。
- 版本号机制:数据表增加
- 典型场景:读多写少、冲突概率低的场景(如电商库存扣减)。
-
悲观锁
- 假设:默认数据会发生冲突,操作前主动加锁。
- 实现方式:
- 代码层:Java 的
synchronized
、ReentrantLock
。 - 数据库层:
SELECT ... FOR UPDATE
(行锁)、LOCK IN SHARE MODE
(共享锁)。
- 代码层:Java 的
- 典型场景:高并发写操作、强一致性要求的场景(如银行转账)。
二、优缺点对比
维度 | 乐观锁 | 悲观锁 |
---|---|---|
性能开销 | 低(无锁,无阻塞) | 高(加锁、释放锁及线程切换) |
并发能力 | 高(允许多线程并行读) | 低(串行化操作) |
数据一致性 | 最终一致(可能重试) | 强一致(实时锁定) |
ABA 问题 | 存在(需版本号解决) | 不存在 |
死锁风险 | 无 | 可能(需合理设计锁顺序) |
三、适用场景选择
-
乐观锁适用场景
- 读多写少:如商品库存查询频繁但扣减较少。
- 低冲突概率:如用户评论、点赞统计。
- 高吞吐需求:需避免锁竞争带来的性能损耗。
-
悲观锁适用场景
- 高并发写操作:如银行转账、票务系统。
- 强一致性要求:如订单支付后的库存扣减。
- 复杂业务逻辑:需保证临界区操作的完整性。
四、实际案例解析
-
乐观锁案例(库存扣减)
-- 表结构增加 version 字段 UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = 2;
- 逻辑:若更新行数为 1 则成功,否则重试或报错。
-
悲观锁案例(银行转账)
BEGIN; SELECT balance FROM accounts WHERE id = 1001 FOR UPDATE; -- 加排他锁 UPDATE accounts SET balance = balance - 200 WHERE id = 1001; COMMIT;
- 逻辑:锁定账户记录,确保转账操作的原子性。
五、混合使用策略
在实际系统中,两者可结合以平衡性能与一致性:
- 分段锁:如
ConcurrentHashMap
对数据分段加锁,减少锁粒度。 - 锁升级:JVM 的
synchronized
从无锁到重量级锁的动态升级。 - 分布式锁:Redis 的
SETNX
结合 CAS 实现高并发控制。
六、总结
- 乐观锁:适合低冲突、高并发读场景,通过版本号或 CAS 实现,但需处理 ABA 问题。
- 悲观锁:适合高冲突、强一致性场景,通过加锁保证安全,但需控制锁范围和粒度。
- 选型原则:冲突少用乐观锁,冲突多用悲观锁,必要时可结合业务需求混合设计。
🤔 参考 13
我们来详细解读一下乐观锁和悲观锁。
核心概念一句话总结
- 悲观锁:假定冲突一定会发生。因此,在操作数据之前,先加锁,确保整个操作过程独占资源。
- 心态:“总是最坏的打算,我先锁上,你们谁都别动,等我用完再说。”
- 乐观锁:假定冲突很少发生。因此,不加锁,直接去操作数据。在更新的时候,再判断一下此期间有没有别人修改过这个数据。
- 心态:“应该没人跟我抢吧?我先改着,更新的时候再看看有没有冲突,有冲突就重试。”
这两种锁是两种不同的并发策略,而非特指某一种技术或工具。
一、悲观锁 (Pessimistic Locking)
-
工作原理:
认为并发操作中,数据冲突的发生是常态。因此,为了保证数据操作的原子性和正确性,在进行任何操作之前,必须先获取对应的锁。这个锁会阻止其他线程同时访问或修改该数据,直到当前线程完成操作并释放锁。 -
实现方式:
- 数据库:
SELECT ... FOR UPDATE
语句。 - Java:使用
synchronized
关键字或ReentrantLock
等显式锁。
- 数据库:
-
优点:
- 简单粗暴:逻辑简单,无需额外判断。
- 保证强一致性:在锁持有期间,能绝对保证数据不被其他线程干扰。
-
缺点:
- 性能开销大:加锁、释放锁、线程上下文切换都会带来额外开销。
- 并发效率低:一个线程持有锁,其他所有需要这个锁的线程都会被挂起等待,在高并发场景下性能急剧下降。
- 可能引发死锁:多个线程互相等待对方释放锁,会导致程序卡死。
-
适用场景:
- 多写场景:冲突频繁发生的环境。
- 临界区代码复杂:需要确保一大段代码原子性执行的场景。
- 对数据一致性要求极高的场景。
二、乐观锁 (Optimistic Locking)
-
工作原理:
认为并发操作中,数据冲突的发生是小概率事件。因此,不对数据加锁,允许多个线程同时访问。但在更新数据时,会判断在此期间数据是否被其他线程修改过(通过版本号、时间戳或当前值等)。如果未被修改,则更新成功;如果已被修改,则更新失败,通常采用重试机制或报错。 -
实现方式:
- CAS(Compare and Swap)算法:这是乐观锁最核心的技术实现。如 Java 中的
AtomicInteger
、AtomicLong
等原子类。 - 版本号机制:
- 数据库:在表中增加一个
version
字段。更新时UPDATE table SET name='new', version=version+1 WHERE id=#id AND version=#old_version
。 - Java:
AtomicStampedReference
(解决了CAS的ABA问题)。
- 数据库:在表中增加一个
- CAS(Compare and Swap)算法:这是乐观锁最核心的技术实现。如 Java 中的
-
优点:
- 性能高:在没有冲突的情况下,省去了加锁、解锁的开销,极大提升了吞吐量。
- 避免了死锁:因为根本不用获取锁。
-
缺点:
- ABA问题:一个值从A变成B,又变回A,CAS检查时会误以为它没变。解决方案是使用版本号(
AtomicStampedReference
)。 - 自旋开销:如果冲突频繁,不断的重试(自旋)会消耗大量CPU资源。
- 只能保证一个变量的原子性:不方便应对多个共享变量的并发场景。
- ABA问题:一个值从A变成B,又变回A,CAS检查时会误以为它没变。解决方案是使用版本号(
-
适用场景:
- 多读少写:这是乐观锁的理想场景,冲突概率低,收益最大。
- 性能敏感:需要追求极高吞吐量的场景,如一些计数器、状态标志位等。
三、核心区别对比
特性 | 悲观锁 | 乐观锁 |
---|---|---|
核心思想 | 认为冲突总会发生,先行加锁 | 认为冲突很少发生,事后检查 |
实现机制 | 锁(synchronized , ReentrantLock , FOR UPDATE ) | CAS、版本号机制 |
线程阻塞 | 会阻塞其他线程 | 不会阻塞其他线程(失败后通常自旋重试) |
性能 | 开销大,并发性低 | 开销小,并发性高 |
适用场景 | 多写场景,冲突频繁 | 多读少写场景,冲突很少 |
举例 | 数据库行锁、synchronized | AtomicInteger 、数据库版本号更新 |
一个生动的比喻
- 悲观锁:就像独木桥。一次只允许一个人过,后面的人必须排队等待前面的人过去。
- 乐观锁:就像自由市场。大家可以同时看同一个商品(读)。但当你想买下它时(写),需要确认一下从你上次看到现在,这个商品是否已经被别人买走了(版本号/CAS检查)。如果被买走了,你就购买失败,需要重新看看最新的商品列表再尝试。
总结
没有一种锁能在所有场景下都表现完美。
- 当你的应用写操作非常频繁,且对数据一致性要求极高时,悲观锁更为合适,即使牺牲一些性能。
- 当你的应用以读操作为主,写操作很少,并且你想要追求极高的性能时,乐观锁是你的不二之选。
在实际开发中,数据库的乐观锁(版本号) 和 Java JUC包中的原子类(CAS) 是乐观锁思想的典型应用,而 synchronized
和 ReentrantLock
则是悲观锁的代表。理解它们的区别和适用场景,是写出高效、正确并发程序的关键。
🤔 参考 14
以下是对乐观锁与悲观锁两种并发控制机制的具体介绍及其核心差异的分析:
核心思想及哲学
-
悲观锁
- 核心思想:认为并发冲突大概率会发生,需在操作前通过排他锁独占资源[2][3][4]。
- 哲学:基于“最坏情况”假设,优先保障数据一致性而非性能[4][5]。
-
乐观锁
- 核心思想:假定冲突较少发生,允许自由读取数据,仅在更新时检查是否被其他线程修改[4][5][8]。
- 哲学:基于“最好情况”假设,优先提升性能,接受少量更新失败的代价[4][5]。
实现方式
-
悲观锁
- 数据库层面:使用
SELECT ... FOR UPDATE
语句对查询结果加锁,阻止其他事务修改数据[1][4][7]。 - 编程层面:通过
synchronized
关键字或ReentrantLock
等显式锁实现[2][4][5]。
- 数据库层面:使用
-
乐观锁
- 版本号机制:在数据表中添加
version
字段,每次更新时校验版本号是否一致[4][6][7][8]。 - CAS算法:通过CPU指令实现原子性比较并交换操作[4][5][6][8]。
- 工具类支持:Java的
AtomicInteger
、ConcurrentHashMap
等基于CAS实现[4][5][6][8]。
- 版本号机制:在数据表中添加
典型应用场景
-
悲观锁适用场景
- 写多读少且冲突频繁:如金融转账、库存扣减等高竞争场景[1][4][5]。
- 强一致性要求:需严格保证数据完整性的操作[4][5]。
-
乐观锁适用场景
- 读多写少且冲突较少:如社交平台动态点赞、商品浏览统计等[4][5][8]。
- 高性能需求:需最大化吞吐量的场景,允许少量重试换取无锁的高并发[4][5][8]。
优点与缺点
-
悲观锁
- 优点:简单易用,强制独占确保强一致性,避免脏写等问题[2][3][4]。
- 缺点:锁竞争激烈时导致线程阻塞和上下文切换,性能开销大;可能引发死锁[2][3][4][5]。
-
乐观锁
- 优点:无锁操作,读操作完全并行,吞吐量高;无死锁风险[4][5][8]。
- 缺点:高并发写场景下重试次数多,消耗CPU资源;存在ABA问题;无法保证多变量操作的原子性[5][6][8]。
高级应用与优化策略
-
乐观锁的重试机制
- 固定次数重试:适用于冲突概率稳定的场景[4][8]。
- 指数退避重试:冲突时等待时间递增(如100ms→200ms→400ms),减少CPU消耗[4][8]。
-
悲观锁的优化
- 缩小锁范围:仅对关键代码块加锁,而非整个方法[4][5]。
- 分段锁:将数据分段加锁(如
ConcurrentHashMap
),提高并发度[4][5]。
-
混合策略
- 读操作使用乐观锁,写操作使用悲观锁[4][5]。
对比总结
特性 | 悲观锁 | 乐观锁 |
---|---|---|
并发策略 | 先加锁再操作,阻塞其他线程 | 无锁操作,更新时检查冲突 |
性能消耗 | 锁竞争和线程阻塞带来额外开销 | 无锁开销,但可能需要多次重试 |
适用场景 | 写操作频繁、冲突概率高的场景 | 读操作频繁、冲突概率低的场景 |
实现复杂度 | 数据库原生支持,实现简单 | 需要额外字段(版本号)或算法支持 |
死锁风险 | 存在死锁可能 | 无死锁风险 |
一致性保证 | 强一致性(加锁期间数据不可变) | 最终一致性(允许更新失败后重试) |
总的来说,乐观锁与悲观锁的核心差异在于对并发冲突的假设和处理方式。悲观锁通过预加锁确保数据独占,适合高冲突场景;乐观锁通过无锁设计和冲突检测提升性能,适合低冲突场景。选择时需综合考虑业务特点、性能需求和一致性要求[4][5][8]。
🤔 参考 15
乐观锁(Optimistic Lock)与悲观锁(Pessimistic Lock) 是两种常见的并发控制机制,用于解决多线程或多用户环境下对共享资源的访问冲突。
一、悲观锁(Pessimistic Lock)
定义:
悲观锁认为并发访问共享资源时,冲突经常发生,因此每次操作前都会先加锁,确保自己操作期间资源不会被其他线程修改。
特点:
- 每次读写数据前都加锁。
- 适合写操作多、冲突频繁的场景。
- 保证强一致性。
- 可能带来性能瓶颈(如线程阻塞、上下文切换)。
实现方式:
- Java 中的
synchronized
关键字。 ReentrantLock
。- 数据库中的行级锁、表锁等。
示例(Java):
synchronized (lockObj) {// 线程安全的代码块
}
二、乐观锁(Optimistic Lock)
定义:
乐观锁认为并发冲突发生的概率较低,因此在操作时不加锁,而是在提交更新时检查是否发生冲突。如果冲突发生,则拒绝操作或重试。
特点:
- 只在提交时检测冲突。
- 适合读多写少、冲突较少的场景。
- 减少锁的使用,提高并发性能。
- 不能保证绝对一致性,可能需要重试机制。
实现方式:
- 版本号机制(Version):每次更新数据时检查版本号是否变化。
- CAS(Compare and Swap):通过比较旧值和当前值来决定是否更新。
示例(数据库):
UPDATE user SET balance = 100, version = 2
WHERE id = 1 AND version = 1;
如果 version 不一致,说明数据被其他事务修改过,本次更新失败。
示例(Java 使用 Atomic 类):
AtomicInteger atomicInt = new AtomicInteger(0);
int expect = atomicInt.get();
while (!atomicInt.compareAndSet(expect, expect + 1)) {expect = atomicInt.get(); // 重试
}
三、乐观锁 vs 悲观锁
特性 | 悲观锁 | 乐观锁 |
---|---|---|
假设 | 总会发生并发冲突 | 冲突很少发生 |
加锁时机 | 操作前加锁 | 提交时检测冲突 |
适合场景 | 写操作多、冲突频繁 | 读操作多、冲突较少 |
性能影响 | 有阻塞,性能较低 | 无阻塞,性能较高 |
实现方式 | synchronized、ReentrantLock、数据库锁等 | 版本号、CAS、时间戳等 |
数据一致性 | 强一致性 | 最终一致性 |
四、总结
- 悲观锁更适用于数据竞争激烈、写操作频繁的场景。
- 乐观锁适用于读多写少、冲突较少的场景,性能更好。
- 在实际开发中,应根据业务场景和数据竞争情况选择合适的并发控制策略。
如数据库中,乐观锁常用于高并发下单、库存等场景;悲观锁则用于金融交易、库存扣减等对一致性要求极高的场景。