目录

乐观锁与悲观锁-学习笔记

乐观锁与悲观锁 学习笔记

在多线程和分布式系统中,锁机制是保证数据一致性和并发控制的关键技术。乐观锁和悲观锁是两种常见的锁策略,它们在不同的场景下有着各自的优势和适用性。本文将详细介绍乐观锁和悲观锁的原理、区别以及代码示例,帮助你更好地理解和选择合适的锁机制。

前言

**悲观锁:假设冲突是常态,因此在操作数据时直接加锁,直到操作完成才释放锁。

乐观锁:假设冲突是少数情况,因此在操作数据时不加锁,但在更新数据时检查数据是否被其他操作修改。**

一、悲观锁

1.1 悲观锁的定义

悲观锁(Pessimistic Locking)是一种基于“悲观”假设的锁机制。它认为在并发环境中,数据冲突是常态,因此在操作数据时会先加锁,直到操作完成才会释放锁。这种方式类似于传统的关系型数据库中的锁机制,通过锁来防止其他线程或事务对数据的并发访问。

1.2 悲观锁的实现方式

悲观锁可以通过数据库的锁机制(如行锁、表锁)或编程语言中的锁(如 Java 中的 synchronized 或 ReentrantLock)来实现。

示例 1:使用 Java 的 synchronized 实现悲观锁
public class PessimisticLockExample {
    private int count = 0;

    // 使用 synchronized 方法锁
    public synchronized void increment() {
        int temp = count;
        temp += 1;
        count = temp;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        PessimisticLockExample example = new PessimisticLockExample();

        // 创建多个线程模拟并发
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            }).start();
        }

        Thread.sleep(2000); // 等待线程执行完成
        System.out.println("Final count: " + example.getCount());
    }
}
示例 2:使用数据库的悲观锁

在 SQL 中,可以通过 SELECT ... FOR UPDATE 语句实现悲观锁,锁定目标行,防止其他事务修改。

-- 假设有一个表 `account`
BEGIN TRANSACTION;

-- 锁定目标行
SELECT balance FROM account WHERE id = 1 FOR UPDATE;

-- 更新余额
UPDATE account SET balance = balance + 100 WHERE id = 1;

COMMIT;

1.3 悲观锁的优缺点

  • 优点
    • 能够有效防止数据冲突,保证数据一致性。
    • 适合写操作频繁的场景。
  • 缺点
    • 锁的开销较大,可能导致性能瓶颈。
    • 容易出现死锁问题。

二、乐观锁

2.1 乐观锁的定义

乐观锁(Optimistic Locking)基于“乐观”假设,认为在大多数情况下,数据冲突是很少发生的。因此,它不会在操作数据时直接加锁,而是通过版本号(Version Number)或时间戳(Timestamp)来检测数据是否被其他线程修改。

2.2 乐观锁的实现方式

乐观锁通常通过以下两种方式实现:

  1. 基于版本号(Version Number) :每次更新数据时,版本号加一,更新时检查版本号是否一致。
  2. 基于时间戳(Timestamp) :记录数据的最后修改时间戳,更新时检查时间戳是否被修改。
示例 1:基于版本号的乐观锁
import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private AtomicInteger version = new AtomicInteger(0);
    private int count = 0;

    public void increment() {
        int currentVersion = version.get();
        while (true) {
            int temp = count;
            temp += 1;

            // 检查版本号是否被其他线程修改
            if (version.compareAndSet(currentVersion, currentVersion + 1)) {
                count = temp;
                break;
            } else {
                // 如果版本号被修改,重新获取版本号并重试
                currentVersion = version.get();
            }
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        OptimisticLockExample example = new OptimisticLockExample();

        // 创建多个线程模拟并发
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            }).start();
        }

        Thread.sleep(2000); // 等待线程执行完成
        System.out.println("Final count: " + example.getCount());
    }
}
示例 2:基于时间戳的乐观锁

在数据库中,可以通过记录时间戳来实现乐观锁。

-- 假设有一个表 `account`,包含 `balance` 和 `last_modified` 时间戳字段
BEGIN TRANSACTION;

-- 获取当前行的版本信息
SELECT balance, last_modified FROM account WHERE id = 1;

-- 更新余额并检查时间戳
UPDATE account 
SET balance = balance + 100, last_modified = NOW()
WHERE id = 1 AND last_modified = '2025-03-14 10:00:00';

COMMIT;

2.3 乐观锁的优缺点

  • 优点
    • 减少了锁的开销,提高了系统性能。
    • 适合读多写少的场景。
  • 缺点
    • 在高并发写操作场景下,可能会导致大量重试,性能下降。
    • 实现相对复杂,需要额外的版本号或时间戳字段。

三、乐观锁与悲观锁的选择

选择乐观锁还是悲观锁,需要根据具体的业务场景来决定:

  1. 读多写少的场景 :优先选择乐观锁,因为它减少了锁的开销,提高了性能。
  2. 写操作频繁的场景 :优先选择悲观锁,因为它能够有效防止数据冲突。
  3. 对性能要求极高的场景 :可以尝试使用乐观锁,但需要仔细评估重试机制对性能的影响。

四、总结

乐观锁和悲观锁是并发控制中的两种重要策略。悲观锁通过加锁来防止数据冲突,适合写操作频繁的场景;乐观锁通过版本号或时间戳来检测数据冲突,适合读多写少的场景。在实际开发中,我们需要根据业务需求和性能要求,合理选择锁机制,以实现高效且可靠的并发控制。

希望本文对大家理解乐观锁和悲观锁有所帮助。如果你有任何疑问或建议,欢迎在评论区留言交流。