java并发之CAS(Compare and swap)
1. 简介
CAS的底层调用native方法,最终是利用CPU的一个特殊指令,该指令由CPU保证了原子性,而且包含多个操作,比如先比较再更新。
原理:
- (1)需要读写的内存值(V)、原值(A)和新值(B)。如果V的值与原值A相匹配,那么把B设置给V,否则处理器不做任何操作。
- (2)无论哪种情况,都返回V内存值。
- (3)原子类里,当失败时,就一直循环,直到成功。
(1)、(2)是在CPU和内存的层面来说的,(3)是在Java层面说的
比如 AtomicInteger中的源码中,就使用到了 CAS:
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;
}
2. CAS的等价代码
自己实现CAS:
/**
* 模拟CAS操作,等价代码
*/
public class SimulatedCAS {
private int value;
/**
* 模拟CAS操作
* @param expectedValue 预期值
* @param newValue 修改值
* @return 原值
*/
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
}
return oldValue;
}
}
在多线程中调用上面自己实现的CAS:
/**
* 两个线程竞争,其中一个修改落败
*/
public class TwoThreadCompetition implements Runnable{
private int value;
/**
* 模拟CAS操作
* @param expectedValue 预期值
* @param newValue 修改值
* @return 旧值
*/
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
System.out.println(Thread.currentThread().getName()+"将值从"+oldValue+"修改成"+newValue);
}
return oldValue;
}
//主函数
public static void main(String[] args) throws InterruptedException {
TwoThreadCompetition r = new TwoThreadCompetition();
r.value=0;//默认值
Thread thread1 = new Thread(r,"线程1");
thread1.start();
thread1.join();
System.out.println(r.value);
Thread thread2 = new Thread(r,"线程2");
thread2.start();
thread2.join();
System.out.println(r.value);
}
@Override
public void run() {
compareAndSwap(0,1);
}
}
打印结果显示,只有第一个线程对 value 进行了修改,第二个线程没有修改,符合CAS的逻辑。
3. 应用场景
(1)乐观锁
乐观锁的实现就是利用CAS原理,比如数据库在修改时,是利用version字段来判断是否应该执行修改,而不是加锁。
(2)并发容器
如ConcurrentHashMap类中的put()方法
…
(4)原子类
这里以 AtomicInteger 的 getAndIncrement()方法 为例剖析如何利用CAS实现原子性的
- AtomicInteger加载
Unsafe
工具,用来直接操作内存
数据 - 用Unsafe工具来实现底层操作
- 用volatile修饰value字段,保证可见性
上面使用到了 Unsafe 类,那么他是个什么呢?
- Unsafe是CAS的核心类。Java一般无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
- valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了
总结:
总之,getAndIncrement() 的实现原理是:先获取 value 在内存中的地址,然后使用该地址拿到原值,再调用 compareAndSwapInt 方法,将内存地址value,原值,和新值传进去,执行 CAS 方法,compareAndSwapInt 是 native 方法,由c++实现,如下
上面的 C++ 代码可以看出:方法中先想办法拿到变量value在内存中的地址;通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存的值。至此,最终完成了CAS的全过程。
4 CAS的缺点
(1)ABA 问题
什么是ABA?
假若一个变量初次读取是A,在compare阶段依然是A,但其实可能从读取A到后面的compare这段过程里,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较那一刻的值是否A,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。
如何解决ABA问题
可以采用数据库的乐观锁方式,采用version版本号
,每次修改就给version版本号+1
(2)自旋时间过长
do-while的自旋循环,如果多线程竞争激烈的情况下,锁一直拿不到,他就会一直自旋,消耗CPU性能。