乐观锁和悲观锁的详细介绍

在多线程编程中,锁(Lock)是为了保证数据的正确性和线程安全性而使用的一种机制。常见的锁有悲观锁(Pessimistic Lock)乐观锁(Optimistic Lock)。它们的主要区别在于处理并发冲突的方式:悲观锁假设会有冲突,而乐观锁则假设不会有冲突,直到发现冲突才进行处理。

悲观锁(Pessimistic Lock)

定义

悲观锁假设在多线程环境中会发生冲突,因此在访问共享资源时会采用锁机制来防止其他线程的干扰。悲观锁通常会锁住资源,直到操作完成,其他线程不能同时访问该资源。

特点

  • 悲观的锁:始终假设会发生冲突,因此在访问数据时对资源加锁,直到操作完成。
  • 阻塞性:当一个线程获取锁时,其他线程必须等待锁释放才能继续执行。
  • 使用场景:适用于事务较长或冲突较为频繁的情况,确保数据的安全性。

常见实现

  1. 数据库悲观锁:使用 SELECT FOR UPDATE 语句来锁定数据库记录,防止其他事务修改相同记录。
  2. Java中的同步锁:使用 synchronizedReentrantLock 来锁定代码块或方法。

具体示例(基于 Java 和数据库):

  1. 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 方法上加锁。无论有多少个线程调用该方法,只有一个线程能够获取到锁,其他线程必须等待。

  1. 数据库中的悲观锁:
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)

定义

乐观锁假设在多线程环境中不会发生冲突,因此不对资源加锁,而是通过版本控制、时间戳等机制,在提交修改时检查数据是否被其他线程修改。如果在提交时发现数据已被修改,乐观锁就会触发冲突,通常会重新尝试或通知用户。

特点

  • 乐观的锁:假设不会发生冲突,因此不会对资源加锁,直到提交时检查数据是否被修改。
  • 非阻塞性:线程不需要等待其他线程释放锁,而是直接进行操作,操作完成后检查是否发生冲突。
  • 使用场景:适用于数据冲突较少的场景,能够提高系统的并发性能。

常见实现

  1. 数据库乐观锁:使用版本号或时间戳字段来检测数据是否被修改。
  2. Java中的乐观锁:使用 CAS(Compare And Swap) 操作来实现乐观锁。

具体示例(基于 Java 和数据库):

  1. 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,表示数据已被修改。

  1. 数据库中的乐观锁(使用版本号)
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;

在数据库中,通常使用版本号来实现乐观锁。在更新操作时,除了更新数据之外,还要检查当前记录的版本号是否与之前读取的版本号一致。如果不一致,说明其他线程已经修改了数据,这时当前操作会失败,通常需要重新尝试或者报告冲突。

乐观锁与悲观锁的对比

锁机制

假设会发生冲突,对共享资源加锁,直到操作完成

假设不会发生冲突,操作完成后再检查是否发生冲突

性能

因为加锁导致等待,性能较差,尤其在冲突多的情况下

不加锁,性能较好,尤其在冲突少的情况下

适用场景

高冲突场景,或者事务需要操作较长时间的数据

低冲突场景,尤其是读多写少的情况

阻塞性

阻塞,其他线程必须等待锁释放

非阻塞,操作完成后检查数据是否被修改

实现方式

使用数据库的 SELECT FOR UPDATE或 Java 的 synchronized

使用版本号、时间戳,或者 CAS(Compare And Swap)

总结

  • 悲观锁 适用于冲突较为频繁的场景,保证了数据的一致性和正确性,但可能会导致较低的性能。
  • 乐观锁 适用于冲突较少的场景,能提高系统的并发性,但需要在提交时进行冲突检测,增加了重试的可能性。
全部评论

相关推荐

昨天 21:37
点赞 评论 收藏
分享
评论
2
1
分享
牛客网
牛客企业服务