乐观锁和悲观锁的详细介绍
在多线程编程中,锁(Lock)是为了保证数据的正确性和线程安全性而使用的一种机制。常见的锁有悲观锁(Pessimistic Lock)和 乐观锁(Optimistic Lock)。它们的主要区别在于处理并发冲突的方式:悲观锁假设会有冲突,而乐观锁则假设不会有冲突,直到发现冲突才进行处理。
悲观锁(Pessimistic Lock)
定义:
悲观锁假设在多线程环境中会发生冲突,因此在访问共享资源时会采用锁机制来防止其他线程的干扰。悲观锁通常会锁住资源,直到操作完成,其他线程不能同时访问该资源。
特点:
- 悲观的锁:始终假设会发生冲突,因此在访问数据时对资源加锁,直到操作完成。
- 阻塞性:当一个线程获取锁时,其他线程必须等待锁释放才能继续执行。
- 使用场景:适用于事务较长或冲突较为频繁的情况,确保数据的安全性。
常见实现:
- 数据库悲观锁:使用
SELECT FOR UPDATE
语句来锁定数据库记录,防止其他事务修改相同记录。 - Java中的同步锁:使用
synchronized
或ReentrantLock
来锁定代码块或方法。
具体示例(基于 Java 和数据库):
- Java 中的悲观锁:
java public class PessimisticLockExample { private static final Object lock = new Object(); public void updateBalance() { synchronized (lock) { // 模拟数据库操作,更新余额 System.out.println(Thread.currentThread().getName() + " is updating balance"); // 执行某些操作,可能是数据库更新操作等 try { Thread.sleep(1000); // 模拟操作的延迟 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println(Thread.currentThread().getName() + " finished updating balance"); } } public static void main(String[] args) { PessimisticLockExample example = new PessimisticLockExample(); // 创建两个线程,尝试同时更新余额 Thread t1 = new Thread(example::updateBalance); Thread t2 = new Thread(example::updateBalance); t1.start(); t2.start(); } }
在这个例子中,synchronized
关键字用于在 updateBalance
方法上加锁。无论有多少个线程调用该方法,只有一个线程能够获取到锁,其他线程必须等待。
- 数据库中的悲观锁:
sql -- 假设表名为 account,包含 balance 字段 BEGIN; -- 锁定所需的记录 SELECT * FROM account WHERE account_id = 1 FOR UPDATE; -- 更新余额 UPDATE account SET balance = balance - 100 WHERE account_id = 1; COMMIT;
在这个数据库例子中,SELECT ... FOR UPDATE
语句会在读取记录的同时对该记录加锁,确保没有其他事务在此期间修改这条记录,避免并发修改的问题。
乐观锁(Optimistic Lock)
定义:
乐观锁假设在多线程环境中不会发生冲突,因此不对资源加锁,而是通过版本控制、时间戳等机制,在提交修改时检查数据是否被其他线程修改。如果在提交时发现数据已被修改,乐观锁就会触发冲突,通常会重新尝试或通知用户。
特点:
- 乐观的锁:假设不会发生冲突,因此不会对资源加锁,直到提交时检查数据是否被修改。
- 非阻塞性:线程不需要等待其他线程释放锁,而是直接进行操作,操作完成后检查是否发生冲突。
- 使用场景:适用于数据冲突较少的场景,能够提高系统的并发性能。
常见实现:
- 数据库乐观锁:使用版本号或时间戳字段来检测数据是否被修改。
- Java中的乐观锁:使用
CAS(Compare And Swap)
操作来实现乐观锁。
具体示例(基于 Java 和数据库):
- Java 中的乐观锁(CAS):
java import java.util.concurrent.atomic.AtomicInteger; public class OptimisticLockExample { private AtomicInteger balance = new AtomicInteger(1000); public void updateBalance() { int expectedValue = balance.get(); int newValue = expectedValue - 100; // CAS 操作,尝试修改 balance if (balance.compareAndSet(expectedValue, newValue)) { System.out.println(Thread.currentThread().getName() + " successfully updated balance to " + newValue); } else { System.out.println(Thread.currentThread().getName() + " failed to update balance, retrying..."); updateBalance(); // 如果 CAS 失败,可以选择重试 } } public static void main(String[] args) { OptimisticLockExample example = new OptimisticLockExample(); // 创建多个线程,模拟并发更新余额 Thread t1 = new Thread(example::updateBalance); Thread t2 = new Thread(example::updateBalance); t1.start(); t2.start(); } }
在这个例子中,AtomicInteger
提供了原子操作,通过 compareAndSet
方法来实现乐观锁。这个方法会将 balance
的值与预期值进行比较,如果相同则更新余额,否则返回 false
,表示数据已被修改。
- 数据库中的乐观锁(使用版本号):
sql -- 假设表结构包含 version 字段 -- 更新操作,首先检查版本号,确保没有其他事务修改该记录 BEGIN; -- 获取当前记录的版本号 SELECT balance, version FROM account WHERE account_id = 1; -- 假设获取的版本号为 1,余额为 1000 UPDATE account SET balance = balance - 100, version = version + 1 WHERE account_id = 1 AND version = 1; -- 如果受影响的记录数为 0,说明版本号不匹配,数据已经被修改 COMMIT;
在数据库中,通常使用版本号来实现乐观锁。在更新操作时,除了更新数据之外,还要检查当前记录的版本号是否与之前读取的版本号一致。如果不一致,说明其他线程已经修改了数据,这时当前操作会失败,通常需要重新尝试或者报告冲突。
乐观锁与悲观锁的对比
锁机制 | 假设会发生冲突,对共享资源加锁,直到操作完成 | 假设不会发生冲突,操作完成后再检查是否发生冲突 |
性能 | 因为加锁导致等待,性能较差,尤其在冲突多的情况下 | 不加锁,性能较好,尤其在冲突少的情况下 |
适用场景 | 高冲突场景,或者事务需要操作较长时间的数据 | 低冲突场景,尤其是读多写少的情况 |
阻塞性 | 阻塞,其他线程必须等待锁释放 | 非阻塞,操作完成后检查数据是否被修改 |
实现方式 | 使用数据库的 | 使用版本号、时间戳,或者 CAS(Compare And Swap) |
总结
- 悲观锁 适用于冲突较为频繁的场景,保证了数据的一致性和正确性,但可能会导致较低的性能。
- 乐观锁 适用于冲突较少的场景,能提高系统的并发性,但需要在提交时进行冲突检测,增加了重试的可能性。