深入理解悲观锁:原理、实现与应用

一、什么是悲观锁?

悲观锁(Pessimistic Locking)是一种并发控制机制,它基于一种"悲观"的假设:认为在多线程或多进程环境下,数据冲突是常态而非例外。因此,在访问数据前,悲观锁会先获取锁,确保在持有锁的期间内,其他线程无法修改数据,从而避免并发冲突。

悲观锁的核心思想可以概括为:"宁可错杀一千,不可放过一个"。它在数据处理前就假设最坏的情况会发生,因此采取预防性的加锁措施。

二、悲观锁的特点

  1. 排他性:一旦某个线程获得锁,其他线程必须等待
  2. 阻塞性:获取锁失败的线程会被阻塞,直到锁被释放
  3. 一致性保证强:能有效避免脏读、不可重复读等问题
  4. 开销较大:加锁、释放锁需要额外资源
  5. 可能引发死锁:不正确的使用可能导致死锁情况

三、悲观锁的实现方式

在Java中,悲观锁主要通过以下方式实现:

  1. synchronized关键字:Java内置的同步机制
  2. ReentrantLock类:Java并发包中的显式锁
  3. 数据库悲观锁:如SELECT ... FOR UPDATE

下面我们分别用代码示例来说明这几种实现方式。

四、synchronized实现悲观锁

synchronized是Java中最基本的悲观锁实现方式,它可以修饰方法或代码块。

1. 同步方法

public class SynchronizedExample {
    private int count = 0;
    
    // 同步方法
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

2. 同步代码块

public class SynchronizedBlockExample {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        // 同步代码块
        synchronized(lock) {
            count++;
        }
    }
    
    public int getCount() {
        synchronized(lock) {
            return count;
        }
    }
}

3. 类级别锁

public class ClassLevelLock {
    private static int count = 0;
    
    public void increment() {
        // 类级别锁
        synchronized(ClassLevelLock.class) {
            count++;
        }
    }
}

五、ReentrantLock实现悲观锁

ReentrantLock是Java并发包(java.util.concurrent.locks)中提供的显式锁实现,它比synchronized更灵活,提供了更多高级功能。

基本用法

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;
        } finally {
            lock.unlock();  // 确保锁被释放
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

尝试获取锁

public boolean tryIncrement() {
    if (lock.tryLock()) {  // 尝试获取锁,不阻塞
        try {
            count++;
            return true;
        } finally {
            lock.unlock();
        }
    }
    return false;
}

public boolean tryIncrementWithTimeout() throws InterruptedException {
    if (lock.tryLock(1, TimeUnit.SECONDS)) {  // 尝试1秒内获取锁
        try {
            count++;
            return true;
        } finally {
            lock.unlock();
        }
    }
    return false;
}

公平锁与非公平锁

// 非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock(); 

// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);

公平锁会按照线程请求锁的顺序来获取锁,而非公平锁则允许"插队",可能提高吞吐量但可能导致某些线程饥饿。

六、数据库悲观锁

在数据库层面,悲观锁通常通过SELECT ... FOR UPDATE语句实现。

JDBC示例

import java.sql.*;

public class DatabasePessimisticLock {
    public void transferMoney(Connection conn, int fromId, int toId, int amount) throws SQLException {
        conn.setAutoCommit(false);
        try {
            // 锁定from账户
            PreparedStatement stmt1 = conn.prepareStatement(
                "SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
            stmt1.setInt(1, fromId);
            ResultSet rs1 = stmt1.executeQuery();
            
            if (!rs1.next() || rs1.getInt("balance") < amount) {
                throw new RuntimeException("Insufficient balance");
            }
            
            // 锁定to账户
            PreparedStatement stmt2 = conn.prepareStatement(
                "SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
            stmt2.setInt(1, toId);
            stmt2.executeQuery();
            
            // 更新from账户
            PreparedStatement stmt3 = conn.prepareStatement(
                "UPDATE accounts SET balance = balance - ? WHERE id = ?");
            stmt3.setInt(1, amount);
            stmt3.setInt(2, fromId);
            stmt3.executeUpdate();
            
            // 更新to账户
            PreparedStatement stmt4 = conn.prepareStatement(
                "UPDATE accounts SET balance = balance + ? WHERE id = ?");
            stmt4.setInt(1, amount);
            stmt4.setInt(2, toId);
            stmt4.executeUpdate();
            
            conn.commit();
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        }
    }
}

JPA/Hibernate示例

@Entity
public class Account {
    @Id
    private Long id;
    private int balance;
    // getters and setters
}

public class AccountService {
    @PersistenceContext
    private EntityManager em;
    
    @Transactional
    public void transferMoney(Long fromId, Long toId, int amount) {
        // 悲观锁查询
        Account fromAccount = em.find(Account.class, fromId, LockModeType.PESSIMISTIC_WRITE);
        Account toAccount = em.find(Account.class, toId, LockModeType.PESSIMISTIC_WRITE);
        
        if (fromAccount.getBalance() < amount) {
            throw new RuntimeException("Insufficient balance");
        }
        
        fromAccount.setBalance(fromAccount.getBalance() - amount);
        toAccount.setBalance(toAccount.getBalance() + amount);
    }
}

七、悲观锁的适用场景

悲观锁最适合以下场景:

  1. 临界区执行时间长:当操作需要较长时间完成时
  2. 冲突频率高:当并发修改的概率很高时
  3. 强一致性要求:需要严格保证数据一致性的场景
  4. 写操作多:写多读少的场景

典型应用场景包括:

  • 银行转账
  • 库存扣减
  • 订单处理
  • 票务系统

八、悲观锁的优缺点

优点:

  1. 实现简单直观
  2. 保证强一致性
  3. 避免脏读、不可重复读等问题
  4. 适合冲突频繁的场景

缺点:

  1. 性能开销大(加锁、释放锁)
  2. 可能引起线程阻塞和等待
  3. 可能导致死锁
  4. 降低系统吞吐量
  5. 不适用于高并发读场景

九、悲观锁与乐观锁的对比

特性

悲观锁

乐观锁

假设

冲突会发生

冲突很少发生

实现方式

加锁

版本号/时间戳

并发性

开销

适用场景

写多读少

读多写少

冲突处理

阻塞等待

回滚重试

示例

synchronized, ReentrantLock

CAS, 版本控制

十、悲观锁的最佳实践

  1. 尽量缩小锁范围:只锁定必要的代码段
  2. 减少锁持有时间:尽快释放锁
  3. 避免嵌套锁:防止死锁
  4. 使用try-finally确保释放锁:防止锁泄漏
  5. 考虑锁的粒度:根据场景选择合适粒度的锁
  6. 文档化锁策略:记录代码中的锁使用方式
// 最佳实践示例
public class BestPracticeExample {
    private final ReentrantLock lock = new ReentrantLock();
    private Map<String, String> cache = new HashMap<>();
    
    public void updateCache(String key, String value) {
        // 只锁定必要的代码段
        lock.lock();
        try {
            cache.put(key, value);
        } finally {
            lock.unlock();
        }
    }
    
    public String getValue(String key) {
        // 读操作可以使用更轻量级的锁
        lock.lock();
        try {
            return cache.get(key);
        } finally {
            lock.unlock();
        }
    }
}

十一、常见问题与解决方案

1. 死锁问题

问题描述

// 线程1
synchronized(resourceA) {
    synchronized(resourceB) {
        // ...
    }
}

// 线程2
synchronized(resourceB) {
    synchronized(resourceA) {
        // ...
    }
}

解决方案

  • 按固定顺序获取锁
  • 使用tryLock()设置超时
  • 使用锁检测工具

2. 锁粒度过大

问题代码

public synchronized void processOrder() {
    // 整个方法被锁定,包括IO操作
    readFromDB();
    calculate();
    writeToDB();
}

改进方案

public void processOrder() {
    // 只锁定必要部分
    synchronized(this) {
        readFromDB();
        calculate();
    }
    writeToDB();
}

3. 锁泄漏

问题代码

public void riskyMethod() throws Exception {
    lock.lock();
    someOperationThatMayThrowException();
    lock.unlock();  // 如果抛出异常,锁不会被释放
}

解决方案

public void safeMethod() throws Exception {
    lock.lock();
    try {
        someOperationThatMayThrowException();
    } finally {
        lock.unlock();
    }
}

十二、总结

悲观锁是一种重要的并发控制机制,它通过预先加锁的方式确保数据一致性。在Java中,我们可以通过synchronized关键字、ReentrantLock类或在数据库层面实现悲观锁。虽然悲观锁会带来一定的性能开销,但在高冲突、强一致性的场景下,仍然是不可或缺的工具。

合理使用悲观锁需要权衡性能与一致性,遵循最佳实践,避免常见陷阱。在系统设计时,应根据具体场景选择最合适的并发控制策略,有时悲观锁与乐观锁的结合使用可能会达到最佳效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凡尘扰凡心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值