CAS
CAS的全称是Compare and Swap,一个CAS操作涉及到以下过程:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1.比较A和V的值是否相等。
2.如果相等,则交换B和V的值。
3.返回交换操作是否成功。
CAS的应用
1.实现原子类
public class Demo_twentyfive {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger=new AtomicInteger();
//相当于i++操作
atomicInteger.getAndIncrement();
}
}
通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作.如下面代码所示:
public class Demo_twentyone {
//public static int count=0;
public static AtomicInteger count=new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
//count++;
count.getAndIncrement();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
//count++;
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
2.实现自旋锁
实现方式如下面伪代码所示:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
CAS中的ABA问题
ABA问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
1.先把num的值读取到oldnum中。
2.比较oldnum的值是否和num相同
3.如果相等,则把Z赋值给num
但是在线程t1执行这个操作,线程t2可能在中途修改了num的值为B,然后又重新修改为了A,因此t1无法区分这个num始终是A还是中间发生了变化,这就是ABA问题。
ABA问题产生的BUG
通常情况下,ABA问题并不会产生BUG,但总有例外,下面以银行取钱的例子来说明。
假设存款中有100元,要取走50元。由线程1和线程2两个线程操作。
1.线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
望更新为 50.
2.线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
3.在线程2 执行之前, 突然又进账了50元。
4.线程2获取当前存款为100,符合预期,再扣50元,于是就产生了BUG,多扣了50元。
解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
CAS 操作在读取旧值的同时, 也要读取版本号。
真正修改的时候:
1.如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1。
2.如果当前版本号高于读到的版本号. 就操作失败。