0
点赞
收藏
分享

微信扫一扫

乐观锁、悲观锁和自旋锁 来了解一下

一、乐观锁(Optimistic Locking)

        乐观锁的核心思想是假设在大多数情况下,读操作和写操作之间不会发生冲突。在进行写操作之前,不会主动加锁,而是在提交时检查是否发生了冲突。一般通过版本号或时间戳来实现。

优点:

  • 并发性好,读操作不受阻塞。
  • 减少了锁的竞争和开销。

缺点:

  • 冲突较多时,需要回滚重试,增加了开销。
  • 无法解决所有并发冲突问题,可能导致数据不一致。
乐观锁基于版本号控制的演示
  1. 数据库表中添加版本号字段:在需要应用乐观锁的表中,新增一个版本号字段(通常是一个整数类型),用于记录数据的版本信息。
  2. 读取数据时获取版本号:在读取数据的时候,同时获取对应数据的版本号。
  3. 更新数据时比较版本号:在更新数据时,比较当前数据库中的版本号和你读取时获取到的版本号是否一致。 
  • 如果一致,说明在你读取数据之后没有其他事务修改过该数据,可以进行更新操作。此时需要增加目标数据版本号的值,可以是递增或其他策略,以标记数据的更新。
  • 如果不一致,说明在你读取数据之后有其他事务修改过该数据,表示发生冲突,你可以根据业务需求进行相应的处理,如数据回滚、重新读取数据等。

示例代码,基于版本号的实现逻辑:

// 假设有一个用户表,包含 id、name 和 version 字段
public class User {
 private int id;
 private String name;
 private int version;


 // getters and setters
 // 更新用户数据
 public void update(String newName) {
  // 假设这里使用 SQL 更新数据,在实际项目中可使用对应的 ORM 框架替代
  // 例如:UPDATE user SET name = newName, version = version + 1 WHERE id = id and version = version
  // 执行更新时,需要传入当前版本号(之前读取的版本号)
  int rowsUpdated = executeUpdateSQL("UPDATE user SET name = ? WHERE id = ? and version = ?", newName, id, version);
  // 判断更新是否成功,受影响的行数是否为1
  if (rowsUpdated == 1) {
   // 更新成功,更新本地版本号
   version++;
  } else {
   // 更新失败,说明发生冲突,根据业务需求进行相应处理
   // 这里简单抛出一个异常
   throw new OptimisticLockException("Update failed due to conflict.");
  }
 }
}

      在上述示例中,User 类代表了一个用户对象,update() 方法演示了使用乐观锁基于版本号的更新逻辑。首先,通过执行更新 SQL 语句,同时传入当前版本号进行比较,判断是否发生冲突。如果更新成功(受影响的行数为1),则将本地的版本号递增;如果更新失败,则抛出一个自定义的异常,表示发生冲突。

请注意,在实际应用中,版本号的字段名、数据库操作等需要根据具体情况进行修改和调整。


二、悲观锁(Pessimistic Locking)

        悲观锁的核心思想是在进行任何操作之前就假定会发生冲突,因此主动加锁,直到操作完成才释放锁。悲观锁适用于写操作较多,冲突较多的场景,通过锁的机制保证数据的一致性。

优点:

  • 可以解决大部分并发冲突问题,确保数据一致性。
  • 具备较强的可靠性和稳定性。

缺点:

  • 性能较差,加锁和释放锁的开销较大。
  • 导致并发性能下降,读操作也需要等待锁的释放。

应用说明:

    在数据库中,悲观锁是一种并发控制机制,用于保护共享数据在事务期间的一致性。它通过在读取或修改数据时锁住资源,以防止其他事务对数据进行修改,从而确保数据的完整性。

  1. 数据库锁机制:

 不同数据库的锁机制有所差异,主要包括行级锁、表级锁和页级锁等。悲观锁的常见实现方式包括以下几种:

  • SELECT ... FOR UPDATE:在事务中使用 SELECT ... FOR UPDATE 语句,会给相应的数据行加上排它锁,其他事务无法修改该数据行,直到当前事务提交或回滚。
  • SELECT ... WITH (UPDLOCK):在事务中使用 SELECT ... WITH (UPDLOCK) 语句,会给相应的数据行加上更新锁,其他事务可以读取该数据行但无法修改,直到当前事务提交或回滚。
  • SET TRANSACTION ISOLATION LEVEL SERIALIZABLE:通过设置事务的隔离级别为 SERIALIZABLE,确保所有的读取和写入操作都在事务中进行,并发读取和写入操作时,会发生锁等待,保证数据的串行访问。

需要根据具体的数据库和需求来选择适合的锁机制。

  1. JDBC 接口实现:

  在 Java 中,使用 JDBC 接口可以实现数据库的悲观锁。

  • 使用 Connection 对象的 setTransactionIsolation() 方法设置事务的隔离级别为 SERIALIZABLE。
  • 在事务中使用 SQL 语句进行加锁,如使用 SELECT ... FOR UPDATE 或其他支持的数据库锁机制。

 示例代码如下:

Connection connection = ...; // 获取数据库连接
  try {
   connection.setAutoCommit(false); // 开启事务
   connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 设置隔离级别
   // 执行加锁的 SQL 语句
   String sql = "SELECT * FROM table_name WHERE ... FOR UPDATE";
   PreparedStatement statement = connection.prepareStatement(sql);
   ResultSet resultSet = statement.executeQuery();
   // 操作数据
   while (resultSet.next()) {
   // ...
   }
   connection.commit(); // 提交事务
  } catch (SQLException e) {
   connection.rollback(); // 回滚事务
  } finally {
   connection.close(); // 关闭连接
  }

需要根据具体的数据库和业务场景来选择适当的悲观锁实现方式,并注意事务的隔离级别、事务范围和锁的粒度,以避免死锁和性能问题。


三、自旋锁(Spin Lock)

      不同于乐观锁和悲观锁为数据库层面的锁,自旋锁是一种基于业务层面的锁,线程在获取锁时,如果发现锁已经被其他线程占用,会进行一段忙等待,不会阻塞当前线程,而是通过循环不断尝试获取锁。

优点:

  • 线程等待时间短,减少了线程上下文切换的开销。
  • 对于锁竞争激烈的场景,可以提高效率。

缺点:

  • 忙等待会占用CPU资源,资源利用率较低。
  • 当锁被持有时间较长时,会导致自旋时间过长,影响性能。

乐观锁、悲观锁和自旋锁是并发控制中常用的机制,每种锁适用于不同的并发场景。选择合适的锁机制需要根据具体的应用需求、并发程度和数据一致性要求进行综合权衡。

    在 Java 中,可以使用 java.util.concurrent.atomic 包下的原子变量类实现自旋锁。自旋锁是一种基于循环等待的锁,当线程尝试获取锁失败时,不会立即阻塞线程,而是使用循环等待的方式尝试获取锁,直到成功获取锁或达到最大尝试次数。

下面是一个使用自旋锁实现的示例代码:

import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
  private AtomicBoolean locked = new AtomicBoolean(false);
  public void lock() {
    // 循环自旋,尝试获取锁
    while (!locked.compareAndSet(false, true)) {
      // 等待一段时间,避免长时间自旋造成CPU的浪费
      Thread.yield();
    }
  }
  public void unlock() {
    locked.set(false);
  }
}

使用示例:

public class SpinLockExample {
  private SpinLock spinLock = new SpinLock();
  private int count = 0;
  public void increment() {
    spinLock.lock();
    try {
      count++;
    } finally {
      spinLock.unlock();
    }
  }
  public int getCount() {
    spinLock.lock();
    try {
      return count;
    } finally {
      spinLock.unlock();
    }
  }
}

    在上述代码中,SpinLock 类使用 AtomicBoolean 类型的 locked 变量来表示锁的状态,lock() 方法调用 compareAndSet(false, true) 尝试获取锁,若成功则继续执行,否则在循环中继续尝试。unlock() 方法将锁的状态设置为 false。

请注意,自旋锁是一种低级别的同步机制,适用于竞争不激烈、锁占用时间短的情况。在高并发环境下或锁占用时间较长时,使用自旋锁可能会导致CPU资源被浪费,因此需要根据具体情况进行选择和调优。

举报

相关推荐

0 条评论