悲观锁和乐观锁是两种在并发编程和数据库操作中用于解决数据竞争和并发冲突问题的锁机制
悲观锁
概念
悲观锁的核心思想是“悲观”地认为在并发环境下,数据随时都可能被其他线程或事务修改。因此,在对数据进行操作之前,会先对数据加锁,防止其他线程或事务对其进行修改,直到操作完成并释放锁。这种锁机制确保了同一时刻只有一个线程或事务可以访问和修改数据,从而避免了并发冲突。
使用场景
- 数据一致性要求极高:在金融系统中进行资金转账操作,对账户余额的修改必须保证高度的一致性和准确性,任何并发冲突都可能导致资金错误,此时适合使用悲观锁。
- 写操作频繁:当多个线程或事务频繁对同一数据进行写操作时,悲观锁可以有效避免数据冲突,保证数据的完整性。
优缺点
- 优点:
- 强一致性:能确保在加锁期间数据不会被其他线程或事务修改,提供了很强的数据一致性保证。
- 简单易用:实现相对简单,开发人员易于理解和使用。
- 缺点:
- 性能开销大:加锁和解锁操作会带来一定的性能损耗,尤其是在高并发场景下,可能会导致系统响应变慢。
- 容易产生死锁:如果多个线程或事务相互等待对方释放锁,可能会导致死锁的发生,影响系统的正常运行。
实现方式
- 数据库层面:在数据库中,常见的悲观锁实现方式是使用
SELECT ... FOR UPDATE
语句。例如,在 MySQL 中:
-- 开启事务
START TRANSACTION;
-- 使用悲观锁查询数据
SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE;
-- 对查询到的数据进行修改操作
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 提交事务
COMMIT;
- 编程语言层面:在 Java 中,可以使用
synchronized
关键字或ReentrantLock
类来实现悲观锁。例如:
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int data = 0;
public void updateData() {
lock.lock();
try {
// 对数据进行修改操作
data++;
} finally {
lock.unlock();
}
}
}
乐观锁
概念
乐观锁的核心思想是“乐观”地认为在并发环境下,数据被其他线程或事务修改的概率比较小。因此,在对数据进行操作时,不会先加锁,而是在更新数据时检查数据是否被其他线程或事务修改过。如果数据没有被修改,则进行更新操作;如果数据已经被修改,则采取相应的措施,如重试或回滚操作。
使用场景
- 读操作频繁:在一些只读场景或读操作远远多于写操作的场景中,使用乐观锁可以减少加锁和解锁的开销,提高系统的性能。
- 并发冲突较少:当多个线程或事务对同一数据的修改冲突较少时,乐观锁可以避免悲观锁带来的性能损耗。
优缺点
- 优点:
- 性能高:由于不需要加锁和解锁操作,乐观锁在并发度较高的场景下可以显著提高系统的性能。
- 减少死锁风险:因为不使用传统的锁机制,所以不会出现死锁的问题。
- 缺点:
- 数据一致性较弱:在并发冲突较高的情况下,可能会出现多次重试或更新失败的情况,导致数据一致性受到一定影响。
- 实现复杂:需要额外的机制来检查数据是否被修改,实现相对复杂。
实现方式
- 版本号机制:在数据库表中增加一个版本号字段,每次更新数据时,将版本号加 1。在更新数据时,检查当前版本号是否与查询时的版本号一致,如果一致则更新数据并更新版本号;如果不一致,则表示数据已经被其他线程或事务修改,需要进行相应的处理。例如,在 MySQL 中:
-- 假设表中有 id、data 和 version 字段
-- 查询数据并记录版本号
SELECT id, data, version FROM products WHERE id = 1;
-- 更新数据时检查版本号
UPDATE products SET data = 'new data', version = version + 1 WHERE id = 1 AND version = 1;
- 时间戳机制:与版本号机制类似,只是使用时间戳来记录数据的修改时间。在更新数据时,检查当前时间戳是否与查询时的时间戳一致,如果一致则更新数据。
下面为你提供使用 C++ 实现悲观锁和乐观锁的示例代码。
悲观锁示例
在 C++ 里,std::mutex
可用于实现悲观锁,借助 lock()
和 unlock()
方法来对共享资源进行保护,保证同一时刻仅有一个线程能访问该资源。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
// 线程函数,使用悲观锁对共享数据进行操作
void increment() {
for (int i = 0; i < 100000; ++i) {
// 加锁
mtx.lock();
++shared_data;
// 解锁
mtx.unlock();
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final shared data value: " << shared_data << std::endl;
return 0;
}
乐观锁
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> shared_data_optimistic(0);
// 线程函数,使用乐观锁对共享数据进行操作
void increment_optimistic()
{
for (int i = 0; i < 100000; ++i) {
int expected = shared_data_optimistic.load();
int desired;
do {
desired = expected + 1;
} while (!shared_data_optimistic.compare_exchange_weak(expected, desired));
}
}
int main() {
std::thread t1(increment_optimistic);
std::thread t2(increment_optimistic);
t1.join();
t2.join();
std::cout << "Final shared data value (optimistic): " << shared_data_optimistic << std::endl;
return 0;
}
代码解释
std::mutex mtx
:定义了一个互斥锁对象mtx
。mtx.lock()
和mtx.unlock()
:在increment
函数里,通过lock()
方法加锁,保证同一时刻只有一个线程能执行++shared_data
操作,操作完成后使用unlock()
方法解锁。
乐观锁示例
乐观锁一般借助原子操作和版本号机制来实现。以下示例使用 std::atomic
类型和比较并交换(CAS)操作实现简单的乐观锁。
代码解释
std::atomic<int> shared_data_optimistic(0)
:定义了一个原子整数shared_data_optimistic
。compare_exchange_weak
:这是一个 CAS 操作,它会比较shared_data_optimistic
的当前值和expected
的值。若相等,就将shared_data_optimistic
的值更新为desired
并返回true
;若不相等,则将expected
更新为shared_data_optimistic
的当前值并返回false
。通过循环不断尝试,直至更新成功。
这两个示例分别展示了 C++ 中悲观锁和乐观锁的基本实现方式,你可以依据实际需求对代码进行修改和扩展。
总结
悲观锁和乐观锁各有优缺点,在实际应用中,需要根据具体的业务场景和并发情况来选择合适的锁机制。如果对数据一致性要求极高且写操作频繁,建议使用悲观锁;如果读操作频繁且并发冲突较少,乐观锁可能是更好的选择。