一、乐观锁(Optimistic Locking)
乐观锁的核心思想是假设在大多数情况下,读操作和写操作之间不会发生冲突。在进行写操作之前,不会主动加锁,而是在提交时检查是否发生了冲突。一般通过版本号或时间戳来实现。
优点:
- 并发性好,读操作不受阻塞。
- 减少了锁的竞争和开销。
缺点:
- 冲突较多时,需要回滚重试,增加了开销。
- 无法解决所有并发冲突问题,可能导致数据不一致。
乐观锁基于版本号控制的演示
- 数据库表中添加版本号字段:在需要应用乐观锁的表中,新增一个版本号字段(通常是一个整数类型),用于记录数据的版本信息。
- 读取数据时获取版本号:在读取数据的时候,同时获取对应数据的版本号。
- 更新数据时比较版本号:在更新数据时,比较当前数据库中的版本号和你读取时获取到的版本号是否一致。
- 如果一致,说明在你读取数据之后没有其他事务修改过该数据,可以进行更新操作。此时需要增加目标数据版本号的值,可以是递增或其他策略,以标记数据的更新。
- 如果不一致,说明在你读取数据之后有其他事务修改过该数据,表示发生冲突,你可以根据业务需求进行相应的处理,如数据回滚、重新读取数据等。
示例代码,基于版本号的实现逻辑:
// 假设有一个用户表,包含 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)
悲观锁的核心思想是在进行任何操作之前就假定会发生冲突,因此主动加锁,直到操作完成才释放锁。悲观锁适用于写操作较多,冲突较多的场景,通过锁的机制保证数据的一致性。
优点:
- 可以解决大部分并发冲突问题,确保数据一致性。
- 具备较强的可靠性和稳定性。
缺点:
- 性能较差,加锁和释放锁的开销较大。
- 导致并发性能下降,读操作也需要等待锁的释放。
应用说明:
在数据库中,悲观锁是一种并发控制机制,用于保护共享数据在事务期间的一致性。它通过在读取或修改数据时锁住资源,以防止其他事务对数据进行修改,从而确保数据的完整性。
- 数据库锁机制:
不同数据库的锁机制有所差异,主要包括行级锁、表级锁和页级锁等。悲观锁的常见实现方式包括以下几种:
- SELECT ... FOR UPDATE:在事务中使用 SELECT ... FOR UPDATE 语句,会给相应的数据行加上排它锁,其他事务无法修改该数据行,直到当前事务提交或回滚。
- SELECT ... WITH (UPDLOCK):在事务中使用 SELECT ... WITH (UPDLOCK) 语句,会给相应的数据行加上更新锁,其他事务可以读取该数据行但无法修改,直到当前事务提交或回滚。
- SET TRANSACTION ISOLATION LEVEL SERIALIZABLE:通过设置事务的隔离级别为 SERIALIZABLE,确保所有的读取和写入操作都在事务中进行,并发读取和写入操作时,会发生锁等待,保证数据的串行访问。
需要根据具体的数据库和需求来选择适合的锁机制。
- 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资源被浪费,因此需要根据具体情况进行选择和调优。