前言
在现代计算机系统中,处理并发操作时,锁机制是至关重要的。本文将介绍乐观锁、悲观锁以及CAS(Compare and Swap)这三种常见的并发控制技术,帮助理解它们的原理和应用场景。
1.悲观锁
1.1 定义
悲观锁是一种在访问共享资源之前,首先对资源进行加锁的机制。它假设在任何时候都可能发生冲突,因此在开始操作之前就先锁定资源,防止其他线程访问。
1.2 特点
- 阻塞式:如果一个线程持有锁,其他线程只能等待。
- 简单直观:实现较为简单,容易理解和使用。
- 性能问题:在高并发场景下,线程会因为等待锁而导致性能下降。
1.3 应用场景
悲观锁适用于写操作频繁的场景,如数据库事务处理。在这种情况下,通过加锁来保护数据的一致性和完整性是很重要的。
像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
public void synchronisedTask() {
// 使用内置的同步机制,锁定当前对象(this)
synchronized (this) {
// 需要同步的操作,这些操作在同一时间只能由一个线程执行
}
}
// 定义一个 ReentrantLock 对象,用于显式锁定
private Lock lock = new ReentrantLock();
lock.lock(); // 尝试获取锁
try {
// 需要同步的操作,这些操作在同一时间只能由一个线程执行
} finally {
// 确保在操作完成后释放锁,避免死锁
lock.unlock();
}
悲观锁图解
2.乐观锁
2.1 定义
乐观锁与悲观锁相反,它不在操作前对资源加锁,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
2.2 特点
- 非阻塞:不需要等待锁,可以提高并发性。
- 版本控制:通常通过版本号或时间戳来检测冲突。
- 重试机制:如果发现冲突,线程会重试操作。
2.3 应用场景
乐观锁适合读操作多、写操作少的场景,比如某些在线应用或缓存系统。由于写操作相对少,冲突的概率低,因此可以利用乐观锁的优势,提高系统性能。
在 Java 中,有一些类和框架使用了乐观锁的思想,例如 java.util.concurrent 包下:
- ConcurrentHashMap:在读取和更新时使用了乐观锁机制,允许多个线程并发访问而不阻塞。
- AtomicReference、AtomicInteger 等原子类:这些类利用 CAS(Compare-And-Swap)机制实现乐观锁,确保在更新值时只有在当前值与预期值相等时才进行修改。
乐观锁图解
3.CAS(Compare And Swap)
3.1 定义
CAS 的全称是 Compare And Swap(比较与交换),CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS图解:
3.2 代码示例
import java.util.concurrent.atomic.AtomicInteger;
public class CasCounter {
// 提供了原子操作,保证在多线程环境中对其值的读取和更新是安全的。原子操作意味着不会被其他线程干扰,因此可以避免竞争条件。
private AtomicInteger count = new AtomicInteger(0);
// 增加计数器的方法
public void increment() {
int currentValue;
int newValue;
while (true) {
currentValue = count.get(); // 获取当前计数值
newValue = currentValue + 1; // 计算新的计数值
// 尝试将当前值更新为新值,只有当当前值未被其他线程修改时才会成功
if (count.compareAndSet(currentValue, newValue)) {
break;
}
}
}
// 获取当前计数器的值
public int getCount() {
return count.get();
}
public static void main(String[] args) {
CasCounter counter = new CasCounter();
// 创建多个线程来增加计数器
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment(); // 每个线程增加1000次
}
});
threads[i].start(); // 启动线程
}
// 等待所有线程完成
for (Thread thread : threads) {
try {
thread.join(); // 等待线程结束
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final count: " + counter.getCount());
// 结果:Final count: 10000
}
}
3.3 存在的问题
- ABA问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,但读取到赋值的这段时间内它的值可能被改为B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。
- 循环开销时间大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
4.版本号机制
4.1 定义
版本号机制一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
4.2 代码示例
-- 读取数据时获取版本号
SELECT name, version FROM users WHERE id = 1;
-- 尝试更新数据,更新时版本号也作为条件
UPDATE users
SET name = 'New Name', version = version + 1
WHERE id = 1 AND version = <original_version>;
总结
- 悲观锁认为共享资源在每次访问时都会发生冲突,因此在每次操作时都会加锁。这种锁机制会导致其他线程阻塞,直到锁被释放。虽然悲观锁能有效避免数据竞争,但在高并发场景下会导致线程阻塞、上下文切换频繁,从而影响系统性能,并且还可能引发死锁问题。
- 乐观锁认为共享资源在每次访问时不会发生冲突,因此无须加锁,只需在提交修改时验证数据是否被其他线程修改。乐观锁避免了线程阻塞和死锁问题,在读多写少的场景中性能优越。但在写操作频繁的情况下,可能会导致大量重试和失败,从而影响性能。
- 乐观锁主要通过版本号机制或 CAS 算法实现。版本号机制通过比较版本号确保数据一致性,而 CAS 通过硬件指令实现原子操作,直接比较和交换变量值。