Skip to content

深入理解乐观锁与悲观锁

在多线程编程中,锁是保证数据一致性的重要机制。本文将详细介绍两种常见的锁策略:乐观锁和悲观锁。

什么是锁?

在多线程环境下,多个线程同时访问共享资源时,可能会导致数据不一致的问题。比如两个线程同时读取同一个账户余额,然后都进行扣款操作,就可能出现重复扣款的情况。

java
// 银行账户类(线程不安全版本)
public class BankAccount {
    private double balance; // 账户余额

    // 构造函数,初始化余额
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // 取款方法
    public void withdraw(double amount) {
        // 检查余额是否充足
        if (balance >= amount) {
            // 模拟业务处理时间(实际场景中可能是数据库操作)
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 扣减余额
            balance -= amount;
            System.out.println("取款成功,剩余余额: " + balance);
        } else {
            System.out.println("余额不足");
        }
    }
}

上面的代码存在线程安全问题,即使余额足够,也可能出现超卖的情况。

悲观锁

概念

悲观锁认为:每次访问共享资源时,都会有其他线程来竞争。所以在访问之前,先把资源锁住,确保只有自己能用完再释放。

就像去图书馆借书,你怕别人也想要这本书,所以一去就先把书锁在柜子里,等你用完了再放回去。

实现方式

Java 中悲观锁主要有两种实现方式:

1. synchronized 关键字

java
// 使用 synchronized 实现的线程安全银行账户
public class SynchronizedBankAccount {
    private double balance; // 账户余额

    // 构造函数,初始化余额
    public SynchronizedBankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // synchronized 保证方法同一时间只能被一个线程访问
    public synchronized void withdraw(double amount) {
        // 检查余额是否充足
        if (balance >= amount) {
            // 模拟业务处理时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 扣减余额
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 余额不足");
        }
    }
}

使用测试:

java
public class SynchronizedDemo {
    public static void main(String[] args) {
        // 创建余额为 1000 的账户
        SynchronizedBankAccount account = new SynchronizedBankAccount(1000);

        // 创建两个线程同时取款 500
        Thread t1 = new Thread(() -> account.withdraw(500), "线程1");
        Thread t2 = new Thread(() -> account.withdraw(500), "线程2");

        // 启动线程
        t1.start();
        t2.start();
    }
}

执行结果:

线程1 取款成功,剩余余额: 500.0
线程2 余额不足

2. ReentrantLock

java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 使用 ReentrantLock 实现的线程安全银行账户
public class LockBankAccount {
    private double balance; // 账户余额
    private final Lock lock = new ReentrantLock(); // 创建可重入锁

    // 构造函数,初始化余额
    public LockBankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void withdraw(double amount) {
        // 获取锁
        lock.lock();
        try {
            // 检查余额是否充足
            if (balance >= amount) {
                // 模拟业务处理时间
                Thread.sleep(100);
                // 扣减余额
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + " 余额不足");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁(放在 finally 中确保一定执行)
            lock.unlock();
        }
    }
}

特点

  • 优点:安全可靠,能保证数据一致性
  • 缺点:性能开销大,锁竞争激烈时会降低系统性能
  • 适用场景:写操作多、竞争激烈的场景

乐观锁

概念

乐观锁认为:每次访问共享资源时,不会有其他线程来竞争。所以不直接加锁,而是在更新数据时检查是否有其他线程修改过。

就像去超市买东西,你觉得货架上商品很充足,不用担心被人抢完,所以直接拿商品去结账。结账时再检查价格和库存,如果有问题再处理。

实现方式

乐观锁主要有两种实现方式:

1. 版本号机制(Version)

给每条数据加一个版本号,更新时检查版本号是否一致。

java
import java.util.concurrent.atomic.AtomicInteger;

// 使用版本号机制实现的乐观锁账户
public class OptimisticAccount {
    private double balance; // 账户余额
    private final AtomicInteger version = new AtomicInteger(0); // 版本号,使用原子类保证线程安全

    // 构造函数,初始化余额
    public OptimisticAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public boolean withdraw(double amount) {
        // 读取当前版本号
        int currentVersion = version.get();
        
        // 模拟业务处理时间
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 检查余额是否充足
        if (balance >= amount) {
            // 扣减余额
            balance -= amount;
            // 使用 CAS 更新版本号和余额
            // compareAndSet 方法原子性地比较并更新值
            if (version.compareAndSet(currentVersion, currentVersion + 1)) {
                System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
                return true;
            } else {
                System.out.println(Thread.currentThread().getName() + " 版本冲突,重试");
                return false;
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 余额不足");
            return false;
        }
    }

    public double getBalance() {
        return balance;
    }
}

更完整的版本号实现:

java
// 使用 synchronized 配合版本号的实现
public class VersionedAccount {
    private double balance; // 账户余额
    private long version; // 版本号

    // 构造函数,初始化余额和版本号
    public VersionedAccount(double initialBalance) {
        this.balance = initialBalance;
        this.version = 0;
    }

    public boolean withdraw(double amount) {
        // 使用 synchronized 保证检查-更新操作的原子性
        synchronized (this) {
            // 检查余额是否充足
            if (balance >= amount) {
                // 模拟业务处理时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 扣减余额并更新版本号
                balance -= amount;
                version++;
                System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
                return true;
            } else {
                System.out.println(Thread.currentThread().getName() + " 余额不足");
                return false;
            }
        }
    }
}

2. CAS(Compare-And-Swap)

CAS 是 CPU 级别的原子操作,Java 中的 AtomicIntegerAtomicLong 等都是基于 CAS 实现的。

java
import java.util.concurrent.atomic.AtomicInteger;

// 使用 CAS 实现的银行账户(乐观锁)
public class CASBankAccount {
    private final AtomicInteger balance; // 使用原子整数保证余额的原子操作

    // 构造函数,初始化余额
    public CASBankAccount(int initialBalance) {
        this.balance = new AtomicInteger(initialBalance);
    }

    public boolean withdraw(int amount) {
        // 循环直到成功或余额不足
        while (true) {
            // 读取当前余额
            int currentBalance = balance.get();
            
            // 检查余额是否充足
            if (currentBalance < amount) {
                System.out.println(Thread.currentThread().getName() + " 余额不足");
                return false;
            }

            // 模拟业务处理时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // CAS 更新:如果当前余额等于读取的余额,则更新为新值
            // compareAndSet 返回 true 表示成功,false 表示失败(被其他线程修改)
            if (balance.compareAndSet(currentBalance, currentBalance - amount)) {
                System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + (currentBalance - amount));
                return true;
            }
            // CAS 失败,说明有其他线程修改了余额,进入下一轮循环重试
        }
    }
}

使用测试:

java
public class CASDemo {
    public static void main(String[] args) {
        // 创建余额为 1000 的账户
        CASBankAccount account = new CASBankAccount(1000);

        // 创建三个线程同时取款 500
        Thread t1 = new Thread(() -> account.withdraw(500), "线程1");
        Thread t2 = new Thread(() -> account.withdraw(500), "线程2");
        Thread t3 = new Thread(() -> account.withdraw(500), "线程3");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

执行结果:

线程1 取款成功,剩余余额: 500
线程2 余额不足
线程3 余额不足

特点

  • 优点:性能高,不阻塞线程,适合读多写少的场景
  • 缺点:ABA 问题,需要处理重试逻辑
  • 适用场景:读操作多、写操作少、竞争不激烈的场景

乐观锁与悲观锁对比

特性悲观锁乐观锁
实现方式synchronized、ReentrantLockCAS、版本号
线程模型阻塞式非阻塞式
适用场景写多、竞争激烈读多、写少
性能开销
复杂度
数据安全较高

实际应用场景

悲观锁适用场景

java
// 电商库存服务(悲观锁实现)
public class InventoryService {
    private int stock; // 库存数量
    private final Lock lock = new ReentrantLock(); // 库存锁

    /**
     * 扣减库存
     * @param quantity 要扣减的数量
     * @return true-扣减成功,false-库存不足
     */
    public boolean reduceStock(int quantity) {
        // 获取锁
        lock.lock();
        try {
            // 检查库存是否充足
            if (stock >= quantity) {
                // 扣减库存
                stock -= quantity;
                System.out.println("扣减库存成功,剩余库存: " + stock);
                return true;
            }
            System.out.println("库存不足");
            return false;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

乐观锁适用场景

java
// 用户服务(乐观锁实现)
public class UserService {
    private int version; // 版本号,用于乐观锁控制

    /**
     * 更新用户信息
     * @param user 要更新的用户对象
     * @return true-更新成功,false-版本冲突,需要重试
     */
    public boolean updateUser(User user) {
        // 读取当前版本号(在业务处理之前)
        int currentVersion = version;
        
        // 业务处理...(例如验证、更新数据等)
        user.setName("新名称");
        user.setUpdateTime(System.currentTimeMillis());
        
        // 更新时检查版本号是否发生变化
        if (version == currentVersion) {
            // 版本一致,更新数据并递增版本号
            version++;
            System.out.println("用户更新成功");
            return true;
        }
        // 版本已过期,说明数据被其他线程修改,需要重试
        System.out.println("版本冲突,更新失败");
        return false;
    }
}

总结

  1. 悲观锁:保守策略,适合竞争激烈、写操作多的场景,能保证数据安全但性能较低

  2. 乐观锁:激进策略,适合读多写少、竞争不激烈的场景,性能高但需要处理重试

  3. 选择建议

    • 写操作多、竞争激烈 -> 悲观锁
    • 读操作多、写操作少 -> 乐观锁
    • 不确定 -> 先用乐观锁,必要时再升级为悲观锁

持续更新中...