Java锁:乐观锁与悲观锁
本文讲解Java中的悲观锁与乐观锁。悲观锁(如synchronized)假定冲突,先加锁再操作;乐观锁(CAS机制)假定无冲突,更新时才验证。文章对比了两者的原理、性能及适用场景。
我们来详细、清晰地讲解一下Java中的乐观锁和悲观锁。
这是一个在并发编程中非常核心和重要的概念。
核心思想:对“冲突”的态度
理解这两种锁最简单的方式,就是从它们的名字入手,它们代表了两种截然不同的并发处理哲学:
- 悲观锁 (Pessimistic Locking):它非常“悲观”,总是假设最坏的情况,即每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。这样别人想拿这个数据就会阻塞,直到它拿到锁。
- 乐观锁 (Optimistic Locking):它非常“乐观”,总是假设最好的情况,即每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下,在此期间别人有没有去更新这个数据。
打个比方:
- 悲观锁:就像去一个只有一个卫生间的公共场所。你进去后,先把门反锁 (
lock),这样可以保证在你使用期间,绝对不会有人闯进来。用完之后,你再把锁打开 (unlock),下一个人才能进去。整个过程是“先加锁,再操作”。 - 乐观锁:就像在图书馆修改一份公共文档。你先把文档内容复制一份到你的草稿纸上(读取原始值和版本号),然后在草稿纸上修改。改完后,你想把你的修改提交回去。提交时,你先检查一下公共文档的版本号是不是和你开始看的时候一样。
- 如果一样,说明没人动过,你就可以安全地把你的修改覆盖上去,并更新版本号。
- 如果不一样,说明在你修改期间,已经有人提交了新版本。你的修改就失败了,你需要重新读取新版本,再次修改,然后尝试提交。整个过程是“不加锁,先操作,提交时验证”。
一、 悲观锁 (Pessimistic Locking)
悲观锁在Java中的实现,主要依赖于Java提供的同步机制。
1. 实现方式
synchronized关键字:这是Java中最基本的悲观锁实现。它可以修饰方法或代码块,确保同一时间只有一个线程能执行被它保护的代码。java.util.concurrent.locks.Lock接口:特别是它的实现类ReentrantLock。它提供了比synchronized更高级、更灵活的锁定功能,例如可中断的锁、尝试获取锁、公平锁等。
这两种方式都是独占锁,即一个线程获取了锁,其他线程就必须等待。
2. 代码示例 (synchronized)
假设我们有一个简单的计数器,需要保证多线程下的线程安全。
public class PessimisticCounter {
private int count = 0;
// 使用 synchronized 关键字修饰方法,实现悲观锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个例子中,任何线程想要调用 increment() 方法,都必须先获得 PessimisticCounter 实例的锁。如果锁已经被其他线程持有,那么当前线程就会被阻塞,直到锁被释放。
3. 优缺点
- 优点:
- 简单易用:
synchronized的使用非常直观。 - 数据一致性强:在锁的保护下,数据的一致性可以得到严格的保证。
- 简单易用:
- 缺点:
- 性能开销大:无论是否存在竞争,都会进行加锁、解锁操作。线程的阻塞和唤醒会涉及到用户态和内核态的切换,这是一个非常耗时的操作。
- 可能导致死锁:如果多个线程互相等待对方持有的锁,就会产生死锁。
- 吞吐量低:因为同一时间只允许一个线程操作,其他线程都在等待,无法并行处理。
二、 乐观锁 (Optimistic Locking)
乐观锁本身不是一种具体的锁,而是一种处理并发的思想。它通常通过 CAS (Compare-And-Swap) 机制来实现。
1. 核心机制:CAS (比较并交换)
CAS 是一种原子操作,它包含三个操作数:
- V:要更新的内存地址(变量)
- A:预期的旧值 (Expected Value)
- B:要更新的新值 (New Value)
操作过程是:当且仅当内存地址 V 的当前值等于预期旧值 A 时,才会将 V 的值更新为新值 B。否则,什么也不做。整个比较和交换的过程是一个原子操作,不会被其他线程中断。
2. 实现方式
在Java中,乐观锁的实现主要依赖于 java.util.concurrent.atomic 包下的一系列原子类。
AtomicInteger,AtomicLong,AtomicBoolean等:对基本数据类型进行原子操作。AtomicReference:对引用类型进行原子操作。
这些类内部都使用了 Unsafe 类的 CAS 方法来实现原子更新。
3. 代码示例 (AtomicInteger)
我们用乐观锁来重写上面的计数器。
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticCounter {
// 使用 AtomicInteger 来存储 count
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
// 使用 CAS 实现无锁更新
while (true) {
int current = count.get(); // 1. 读取当前值 (A)
int next = current + 1; // 2. 计算新值 (B)
// 3. 尝试用 CAS 更新
// 如果 count 的当前值仍然是 current,就把它更新为 next,并返回 true
// 如果在 1 和 3 之间,有其他线程修改了 count 的值,那么这里的 CAS 操作就会失败,返回 false
if (count.compareAndSet(current, next)) {
// 更新成功,退出循环
break;
}
// 如果 CAS 失败,说明有竞争,循环会继续,进行下一次尝试(重试)
}
}
public int getCount() {
return count.get();
}
}
这个 while(true) 循环是一种常见的 CAS 使用模式,通常被称为“自旋”。如果更新失败,它会不断地重试,直到成功为止。
4. 优缺点
- 优点:
- 性能好,吞吐量高:在并发冲突不激烈的情况下,避免了线程阻塞和上下文切换的开销,性能远超悲观锁。
- 无死锁问题:因为没有“锁定”资源,所以不会产生死锁。
- 缺点:
- CPU消耗:如果并发冲突非常激烈,会导致大量线程反复重试(自旋),这会消耗大量 CPU 资源。
- ABA 问题:这是乐观锁的一个经典问题。
- 问题描述:一个变量的值原来是A,变成了B,然后又变回了A。另一个线程在做CAS操作时,发现它的值仍然是A,就认为它没有被修改过,于是操作成功。但实际上,这个值中间是发生过变化的。在某些场景下,这会导致严重的问题。
- 解决方案:使用版本号机制。在变量前面追加上版本号,每次变量更新的时候把版本号加一。
java.util.concurrent.atomic.AtomicStampedReference就是用来解决ABA问题的,它将一个“版本戳(stamp)”和引用绑定在一起。
- 只能保证单个共享变量的原子操作:对于多个共享变量的操作,CAS 无法直接保证原子性。需要使用
AtomicReference将多个变量封装成一个对象来进行原子更新,或者使用更复杂的无锁数据结构。
总结与对比
| 特性 | 悲观锁 (Pessimistic Locking) | 乐观锁 (Optimistic Locking) |
|---|---|---|
| 核心思想 | 总是假设有冲突,先加锁再操作。 | 总是假设无冲突,先操作,提交时验证。 |
| 数据一致性 | 依靠独占锁来强制保证。 | 依靠 CAS 和版本号机制来验证保证。 |
| Java实现 | synchronized, java.util.concurrent.locks.Lock |
java.util.concurrent.atomic 包下的原子类 |
| 性能 | 线程阻塞和唤醒开销大,在冲突激烈时表现可能更好。 | 无阻塞,无上下文切换,在冲突不激烈时性能极高。 |
| 适用场景 | 写多读少,并发冲突非常激烈的场景。对数据一致性要求极高,且临界区代码执行时间短。 | 读多写少,并发冲突不激烈的场景。追求高吞吐量。 |
| 缺点 | 性能差,可能产生死锁。 | 冲突激烈时CPU空转严重,存在ABA问题,对复杂场景支持不佳。 |
如何选择?
- 竞争激烈程度:
- 如果资源竞争非常激烈,线程等待的概率很高,使用悲观锁可能更合适。因为乐观锁的不断重试会消耗大量CPU,而悲观锁直接让线程休眠,CPU可以去做别的事情。
- 如果资源竞争不激烈,或者大部分是读操作,使用乐观锁可以获得更高的吞吐量和性能。
- 业务场景:
- 对于更新频率极高、且必须保证数据绝对正确(例如金融领域的账户余额),悲观锁是更稳妥的选择。
- 对于一些允许失败和重试的场景(例如Web应用的库存扣减,失败后可以提示用户“操作失败,请重试”),乐观锁是很好的选择。
在现代的高并发应用中,乐观锁(特别是CAS)的应用越来越广泛,因为它在大多数情况下能提供更好的性能。Java并发包(JUC)中的许多高级工具,如 ConcurrentHashMap, ThreadPoolExecutor 等,其底层都大量使用了CAS技术来实现高效的无锁或低锁并发。