数据并发的双雄:乐观锁与悲观锁深度解析

126 阅读4分钟

在多线程或多进程的环境中,数据的并发访问可能会导致不一致性和错误。为了确保数据的完整性和一致性,锁机制成为了一种重要的解决方案。在锁的领域中,乐观锁和悲观锁是两种常见的策略,它们各自有着不同的特点和适用场景。

一、悲观锁

(一)概念

悲观锁,顾名思义,持有一种悲观的态度,它总是假设在数据处理过程中会有其他并发操作来干扰,导致数据出错。因此,在获取数据时就直接对数据进行加锁,阻止其他并发操作的访问,直到当前操作完成并释放锁。

(二)实现方式

在关系型数据库中,常见的悲观锁实现方式如 SELECT... FOR UPDATE 语句。在编程语言中,可以通过互斥锁(Mutex)、读写锁(ReadWriteLock)等机制来实现。

(三)适用场景

适用于并发冲突频繁、数据竞争激烈的场景。例如,在银行转账等对数据一致性要求极高的操作中,悲观锁可以有效地保证数据的正确性。

二、乐观锁

(一)概念

与悲观锁相反,乐观锁采取一种乐观的态度,它假设在数据处理过程中很少会有并发冲突。因此,在获取数据时不会加锁,而是在更新数据时检查数据是否被其他并发操作修改过,如果没有,则进行更新;如果已被修改,则重新获取数据并尝试更新。

(二)实现方式

常见的实现方式有版本号控制和时间戳机制。

  1. 版本号控制:在数据表中添加一个版本号字段,每次更新数据时版本号加 1。更新时检查版本号是否与获取数据时的版本号一致,如果一致则更新,并将版本号加 1;否则表示数据已被其他操作修改,更新失败。
  2. 时间戳机制:类似版本号,使用时间戳记录数据的修改时间,更新时进行比较。

(三)适用场景

适用于并发冲突较少、读操作远多于写操作的场景。例如,商品库存的查询和扣减,在库存充足的情况下,并发冲突相对较少,使用乐观锁可以提高并发性能。

三、代码示例

(一)悲观锁(以 Java 中的 synchronized 为例)

public class MoreIntuitivePessimisticLockExample {

    private int count = 0;

    //当一个方法被声明为 synchronized 时,意味着在同一时刻,只有一个线程能够执行这个方法。
    // 加锁的方法来增加计数
    public synchronized void increment() {
        count++;
    }

    // 获取计数值的方法
    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        MoreIntuitivePessimisticLockExample example = new MoreIntuitivePessimisticLockExample();

        // 创建并启动第一个线程
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        }).start();

        // 创建并启动第二个线程
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        }).start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.getCount());
    }
}

(二)乐观锁(以版本号控制为例)

public class OptimisticLockExample {

    // 存储值
    private int value;
    // 版本号,用于乐观锁控制
    private int version;

    /**
     * 构造函数,初始化值和版本
     * @param initialValue 初始值
     */
    public OptimisticLockExample(int initialValue) {
        this.value = initialValue;
        this.version = 0;
    }

    /**
     * 获取当前值
     * @return 当前值
     */
    public int getValue() {
        return value;
    }

    /**
     * 获取当前版本号
     * @return 当前版本号
     */
    public int getVersion() {
        return version;
    }

    /**
     * 尝试更新值
     * @param newValue 新的值
     * @param expectedVersion 期望的版本号
     * @return 是否更新成功
     */
    public boolean updateValue(int newValue, int expectedVersion) {
        // 如果当前版本号与期望的版本号一致,则更新值并增加版本号,返回更新成功
        if (version == expectedVersion) {
            value = newValue;
            version++;
            return true;
        }
        // 否则返回更新失败
        return false;
    }

    /**
     * 主函数,创建并启动两个线程尝试更新值
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        OptimisticLockExample example = new OptimisticLockExample(10);

        new Thread(() -> {
            // 获取当前版本号
            int expectedVersion = example.getVersion();
            // 尝试更新值
            boolean updated = example.updateValue(20, expectedVersion);
            if (updated) {
                System.out.println("Thread 1 updated successfully.");
            } else {
                System.out.println("Thread 1 update failed.");
            }
        }).start();

        new Thread(() -> {
            // 获取当前版本号
            int expectedVersion = example.getVersion();
            // 尝试更新值
            boolean updated = example.updateValue(30, expectedVersion);
            if (updated) {
                System.out.println("Thread 2 updated successfully.");
            } else {
                System.out.println("Thread 2 update failed.");
            }
        }).start();
    }
}

四、总结

悲观锁和乐观锁是两种不同的并发控制策略,各有优劣。在实际应用中,需要根据具体的业务场景和并发情况来选择合适的锁策略,以达到最优的性能和数据一致性。