CAS (Compare And Set)比较并替换
上篇文章的锁问题解决,可以使用更高效的方法,使用AtomXXX类,AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原于性的。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
AtomicInteger count = new AtomicInteger(0);
void m1 (){
for(int i=0;i<10000;i++){
count.incrementAndGet(); // 相当于线程安全的 count++
}
}
public static void main(String[] args) {
AtomicIntegerTest t = new AtomicIntegerTest();
List<Thread> Threads = new ArrayList<>();
for(int i=0;i<10;i++){
Threads.add(new Thread(t::m1,"Thread-"+i));
}
Threads.forEach((o) -> o.start());
Threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
结果:
100000
那为什么 count.incrementAndGet(); 是线程安全的? 点进去看它的源码可知:
底层使用了 weakCompareAndSetInt(o, offset, v, v + delta) 方法,这个方法就是 CAS。
CAS 概念:
cas方法一共有三个参数:
V:要改的值(内存位置)
Expected:期望值(预期原值)
NewValue:要设定的新值
如果 要改的值 v 和 expected 的值是一样的,则将 v的值改为新值 newValue。如果不是一样的,则循环下一次比较或直接返回失败。
注:因为 cas方法是 CPU原语支持的,即 在比较替换的过程中是不可以被打断的,所以不会出现 比较成功的时候,其他线程将要改的值或者新值替换的问题。
ABA问题
aba问题是:在进行比较并替换的时候,将a值改为b,然后马上将b再改回a,这样的话,比较是可以成功的,但是对于原值a,它的版本已经不是原始的了。
如果这个a的值是基础类型则没什么关系。但是如果是Object类型,比如:a引用 b,b引用c,这是一个线程将 a引用到了c,并将c的一些属性做了修改,再将a引用到b。这个时候,业务执行的一些逻辑会导致各种问题的出现。如果还没懂,引入一个经典的例子:
一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆嘛?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。
那怎么解决呐?
案例重现:
private static AtomicInteger index = new AtomicInteger(10);
public static void main(String[] args) {
new Thread(() -> {
index.compareAndSet(10, 11);
index.compareAndSet(11, 10);
System.out.println(Thread.currentThread().getName()+
": 10->11->10");
},"张三").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
boolean isSuccess = index.compareAndSet(10, 12);
System.out.println(Thread.currentThread().getName()+
": index是否为预期值:10,"+isSuccess
+" 设置的新值是:"+index.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"李四").start();
}
张三: 10->11->10
李四: index是否为预期值:10,true 设置的新值是:12
通过:AtomicStampedReference 添加版本号 解决这个问题
private static AtomicInteger index = new AtomicInteger(10);
static AtomicStampedReference<Integer> stampRef
= new AtomicStampedReference(10, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = stampRef.getStamp();
System.out.println(Thread.currentThread().getName()
+ " 第1次版本号: " + stamp);
stampRef.compareAndSet(10, 11,stampRef.getStamp(),stampRef.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+ " 第2次版本号: " + stampRef.getStamp());
stampRef.compareAndSet(11, 10,stampRef.getStamp(),stampRef.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+ " 第3次版本号: " + stampRef.getStamp());
},"张三").start();
new Thread(() -> {
try {
int stamp = stampRef.getStamp();
System.out.println(Thread.currentThread().getName()
+ " 第1次版本号: " + stamp);
TimeUnit.SECONDS.sleep(2);
boolean isSuccess =stampRef.compareAndSet(10, 12,
stampRef.getStamp(),stampRef.getStamp()+1);
System.out.println(Thread.currentThread().getName()
+ " 修改是否成功: "+ isSuccess+" 当前版本 :" + stampRef.getStamp());
System.out.println(Thread.currentThread().getName()
+ " 当前实际值: " + stampRef.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"李四").start();
}
张三 第1次版本号: 1
李四 第1次版本号: 1
张三 第2次版本号: 2
张三 第3次版本号: 3
李四 修改是否成功: true 当前版本 :4
李四 当前实际值: 12
这里使用的是AtomicStampedReference的compareAndSet函数,这里面有四个参数:
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)。
(1)第一个参数expectedReference:表示预期值。
(2)第二个参数newReference:表示要更新的值。
(3)第三个参数expectedStamp:表示预期的时间戳。
(4)第四个参数newStamp:表示要更新的时间戳。
重现案例即解决方法原文地址:解决CAS机制中ABA问题的AtomicStampedReference详解 - 知乎 (zhihu.com)
总结:所有以 AtomXXX开头的类,底层都是使用cas方法,并是通过 Unsafe类实现的。
Unsafe类的出现等于c/c++的指针,给 java语言赋予了 原来 C/C++实现的指针方法。比如:
allocateMemory 、freeMemory等操作内存的方法。