在多线程或多进程的环境中,数据的并发访问可能会导致不一致性和错误。为了确保数据的完整性和一致性,锁机制成为了一种重要的解决方案。在锁的领域中,乐观锁和悲观锁是两种常见的策略,它们各自有着不同的特点和适用场景。
一、悲观锁
(一)概念
悲观锁,顾名思义,持有一种悲观的态度,它总是假设在数据处理过程中会有其他并发操作来干扰,导致数据出错。因此,在获取数据时就直接对数据进行加锁,阻止其他并发操作的访问,直到当前操作完成并释放锁。
(二)实现方式
在关系型数据库中,常见的悲观锁实现方式如 SELECT... FOR UPDATE 语句。在编程语言中,可以通过互斥锁(Mutex)、读写锁(ReadWriteLock)等机制来实现。
(三)适用场景
适用于并发冲突频繁、数据竞争激烈的场景。例如,在银行转账等对数据一致性要求极高的操作中,悲观锁可以有效地保证数据的正确性。
二、乐观锁
(一)概念
与悲观锁相反,乐观锁采取一种乐观的态度,它假设在数据处理过程中很少会有并发冲突。因此,在获取数据时不会加锁,而是在更新数据时检查数据是否被其他并发操作修改过,如果没有,则进行更新;如果已被修改,则重新获取数据并尝试更新。
(二)实现方式
常见的实现方式有版本号控制和时间戳机制。
- 版本号控制:在数据表中添加一个版本号字段,每次更新数据时版本号加 1。更新时检查版本号是否与获取数据时的版本号一致,如果一致则更新,并将版本号加 1;否则表示数据已被其他操作修改,更新失败。
- 时间戳机制:类似版本号,使用时间戳记录数据的修改时间,更新时进行比较。
(三)适用场景
适用于并发冲突较少、读操作远多于写操作的场景。例如,商品库存的查询和扣减,在库存充足的情况下,并发冲突相对较少,使用乐观锁可以提高并发性能。
三、代码示例
(一)悲观锁(以 Java 中的 synchronized 为例)
public class MoreIntuitivePessimisticLockExample {
private int count = 0;
//当一个方法被声明为 synchronized 时,意味着在同一时刻,只有一个线程能够执行这个方法。
// 加锁的方法来增加计数
public synchronized void increment() {
count++;
}
// 获取计数值的方法
public int getCount() {
return count;
}
public static void main(String[] args) {
MoreIntuitivePessimisticLockExample example = new MoreIntuitivePessimisticLockExample();
// 创建并启动第一个线程
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
}).start();
// 创建并启动第二个线程
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + example.getCount());
}
}
(二)乐观锁(以版本号控制为例)
public class OptimisticLockExample {
// 存储值
private int value;
// 版本号,用于乐观锁控制
private int version;
/**
* 构造函数,初始化值和版本
* @param initialValue 初始值
*/
public OptimisticLockExample(int initialValue) {
this.value = initialValue;
this.version = 0;
}
/**
* 获取当前值
* @return 当前值
*/
public int getValue() {
return value;
}
/**
* 获取当前版本号
* @return 当前版本号
*/
public int getVersion() {
return version;
}
/**
* 尝试更新值
* @param newValue 新的值
* @param expectedVersion 期望的版本号
* @return 是否更新成功
*/
public boolean updateValue(int newValue, int expectedVersion) {
// 如果当前版本号与期望的版本号一致,则更新值并增加版本号,返回更新成功
if (version == expectedVersion) {
value = newValue;
version++;
return true;
}
// 否则返回更新失败
return false;
}
/**
* 主函数,创建并启动两个线程尝试更新值
* @param args 命令行参数
*/
public static void main(String[] args) {
OptimisticLockExample example = new OptimisticLockExample(10);
new Thread(() -> {
// 获取当前版本号
int expectedVersion = example.getVersion();
// 尝试更新值
boolean updated = example.updateValue(20, expectedVersion);
if (updated) {
System.out.println("Thread 1 updated successfully.");
} else {
System.out.println("Thread 1 update failed.");
}
}).start();
new Thread(() -> {
// 获取当前版本号
int expectedVersion = example.getVersion();
// 尝试更新值
boolean updated = example.updateValue(30, expectedVersion);
if (updated) {
System.out.println("Thread 2 updated successfully.");
} else {
System.out.println("Thread 2 update failed.");
}
}).start();
}
}
四、总结
悲观锁和乐观锁是两种不同的并发控制策略,各有优劣。在实际应用中,需要根据具体的业务场景和并发情况来选择合适的锁策略,以达到最优的性能和数据一致性。