乐观锁是一种并发控制的策略,它假设多个线程在操作共享数据时,不会发生冲突,因此不需要加锁,而是在更新数据时,通过比较当前状态和上一次的状态,来判断是否有其他线程修改了数据。如果没有冲突,就执行更新操作,否则就重试或者放弃。乐观锁的优点是减少了锁的开销,提高了并发性能;缺点是可能会造成大量的重试开销,以及存在ABA问题。
Java中的乐观锁主要有两种实现方式:**CAS(Compare and Swap)**和**版本号控制**。
## CAS(Compare and Swap)
CAS是一种无锁的原子操作,它利用CPU指令来实现数据的比较和替换。CAS操作需要三个参数:内存地址V,旧的预期值A,要修改的新值B。当执行CAS操作时,会先比较内存地址V中的值是否等于旧的预期值A,如果相等,就将V中的值替换为新值B,并返回成功;如果不相等,就返回失败,并重新获取V中的最新值。这个过程是原子的,不会被其他线程干扰。
Java中提供了一系列的原子类,如AtomicInteger, AtomicLong, AtomicReference等,它们都使用了CAS操作来实现原子性。例如,AtomicInteger类中的incrementAndGet方法就是通过CAS来实现自增操作的:
java
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
```
其中unsafe是一个sun.misc.Unsafe类的实例,它可以直接操作内存地址。valueOffset是一个long型的变量,表示value字段在AtomicInteger对象中的偏移量。getAndAddInt方法就是使用CAS来实现加法操作的:
```java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
```
可以看到,这个方法使用了一个自旋循环(do-while),不断地获取当前值var5,并尝试用CAS将其加上var4,并替换原来的值。如果CAS成功,就返回var5;如果失败,就继续循环。
CAS的优点是无锁、高效、线程安全;缺点是可能会导致ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。
### ABA问题
ABA问题是指,在一个线程执行CAS操作时,发现内存中的值没有变化(仍然为A),就认为没有其他线程修改过这个值,然后执行更新操作。但是实际上,在这个线程获取到A之后,另一个线程已经将A修改为B,并且又修改回A了。这样就造成了一个误判,因为这个值虽然没有变化,但是已经不是原来那个值了。
解决ABA问题的一种方法是使用版本号控制。每次修改数据时,都给数据加上一个版本号(或者时间戳),这样就可以区分出不同的修改历史。Java中提供了一个AtomicStampedReference类,它可以保存一个带有版本号(或者时间戳)的引用类型数据。它提供了一个compareAndSet方法,可以同时比较引用和版本号,并进行原子更新:
```java
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
```
这个方法需要四个参数:期望的引用expectedReference,新的引用newReference,期望的版本号expectedStamp,新的版本号newStamp。如果当前的引用和版本号都和期望的一致,就将引用和版本号更新为新的值,并返回true;否则返回false。
## 版本号控制
版本号控制是另一种实现乐观锁的方式,它主要用于数据库层面的并发控制。版本号控制的思想是,在数据表中增加一个版本号字段(或者时间戳字段),每次更新数据时,都将版本号加一(或者更新时间戳),并且在更新时,检查当前的版本号是否和数据库中的一致,如果一致,就执行更新操作;如果不一致,就放弃更新或者抛出异常。
例如,假设有一个用户表user,其中有一个字段version表示版本号。当一个线程想要更新用户信息时,它会先查询出用户的当前信息,包括version字段。然后,在更新时,它会带上version字段作为条件,例如:
```sql
update user set name = 'Tom', version = version + 1 where id = 1 and version = 1;
```
这样,如果数据库中的version字段仍然为1,就说明没有其他线程修改过这条数据,就可以执行更新操作,并将version字段加一;如果数据库中的version字段已经不是1了,就说明有其他线程修改过这条数据,就不会执行更新操作。
版本号控制的优点是简单易用,适用于任何支持SQL的数据库;缺点是需要额外增加一个字段,并且在高并发情况下,可能会造成大量的更新失败。