一、什么是CAS
CAS 是 Compare And Swap的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。
在 Java的原子类中,有对cas的封装进行应用:此处用 AtomicInteger 举例
public class CAS_Demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//CAS的封装体现:compareAndSet 比较并赋值
//比较当前值是否为2020? set 2021,return true : do nothing,return false
System.out.println(atomicInteger.compareAndSet(2020, 2021));//true
System.out.println(atomicInteger.get());//此时为2021
//获取值并自增
atomicInteger.getAndIncrement();//自增后为 2022
System.out.println(atomicInteger.compareAndSet(2021, 2033));//false
System.out.println(atomicInteger.get());// 2022
}
}
点击查看AtomicInteger的源码:
直接通过Unsafe类调用底层 CAS方法
//compareAndSet
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//getAndIncrement
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
getAndAddInt方法详解:
通过一定的轮询,也就是后面介绍的自旋锁
CAS锁其又称作Java的乐观锁,但是我们可以看到其实质上是没有上锁的,只是赋值的过程前多了一个比较的方法,因此可能引起一定的ABA问题:
[狸猫换太子] 如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。
举个栗子:
在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。
public class CAS_Demo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 我们想把数字改成6666
// ============== 捣乱的线程 ==================
atomicInteger.compareAndSet(2020, 2021);
System.out.println("such a fool...");
atomicInteger.compareAndSet(2021, 2020);
// ============== 期望的线程 ==================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
解决上述问题的一个策略是:
每一次倒水假设有一个自动记录仪记录下,也就是给操作赋值上一个标致,这样主人回来就可以分辨在他离开后是否发生过重新倒满的情况。
这也是解决ABA问题目前采用的策略,这就需要我们的原子引用
二、原子引用
官方说明:
一个AtomicStampedReference维护对象引用以及整数“印记”,可以原子更新。
实现注意事项:此实现通过创建表示“boxed”[引用,整数]对的内部对象来维护加盖引用。
举个例子:
这里用到了双参构造方法:(一个初始值,一个时间戳)
public class CAS_ABA_Demo {
public static void main(String[] args) {
AtomicStampedReference<Integer> intAtomicStampedReference = new AtomicStampedReference<>(1, 1);
//A线程
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "操作前stamp:A1==>"+ intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ignored) {}
intAtomicStampedReference.compareAndSet(1,2,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "操作后stamp:A2==>"+ intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
intAtomicStampedReference.compareAndSet(2,1,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "回滚操作stamp:A3==>"+ intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
},"线程A ").start();
//B线程
new Thread(()->{
int stamp = intAtomicStampedReference.getStamp(); // 获得版本号
System.out.println(Thread.currentThread().getName() + "操作前stamp:B=>"+ stamp + " 值:"+intAtomicStampedReference.getReference());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ignored) {}
System.out.println(Thread.currentThread().getName() + "操作是否成功:"+intAtomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println(Thread.currentThread().getName() + "操作后stamp:b2=>"+intAtomicStampedReference.getStamp() + " 值:"+intAtomicStampedReference.getReference());
},"线程B ").start();
}
}
执行结果:
线程A 操作前stamp:A1==>1 值:1
线程B 操作前stamp:B=>1 值:1
线程A 操作后stamp:A2==>2 值:2
线程A 回滚操作stamp:A3==>3 值:1
线程B 操作是否成功:false
线程B 操作后stamp:b2=>3 值:1
线程B因为版本stamp不一致的问题的问题,导致无法修改成功。
此时就不会由于stamp,也可理解为数据库中乐观锁的版本号,时的赋值操作失败,修改不成功。
三、Integer包装类的小坑
在原子引用的示例中,是由1变为2,再2变为1。若将测试示例改为由2021变为2022,再2022变为2021。会发现示例测试无法成功,版本号不会改变。
public class CAS_ABA_Demo {
public static void main(String[] args) {
AtomicStampedReference<Integer> intAtomicStampedReference = new AtomicStampedReference<>(2021, 1);
//A线程
new Thread(()->{
System.out.println("操作前stamp:A1==>"+ intAtomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ignored) {}
intAtomicStampedReference.compareAndSet(2021,2022,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
System.out.println("操作后stamp:A2==>"+ intAtomicStampedReference.getStamp());
intAtomicStampedReference.compareAndSet(2022,2021,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
System.out.println("回滚操作stamp:A3==>"+ intAtomicStampedReference.getStamp());
System.out.println("假装是 x="+intAtomicStampedReference.getReference());
},"A").start();
//B线程
new Thread(()->{
int stamp = intAtomicStampedReference.getStamp(); // 获得版本号
System.out.println("操作前stamp:B=>"+ stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ignored) {}
System.out.println("操作是否成功:"+intAtomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("操作后stamp:b2=>"+intAtomicStampedReference.getStamp());
System.out.println("此时的值:"+intAtomicStampedReference.getReference());
},"B").start();
}
}
结果:
操作前stamp:A1==>1
操作前stamp:B=>1
操作后stamp:A2==>1
回滚操作stamp:A3==>1
假装是 x=2021
操作是否成功:false
操作后stamp:b2=>1
此时的值:2021
先看看intAtomicStampedReference.compareAndSet源码:
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)));
}
current.reference取得的是初始化2021,不清楚的可以看下AtomicStampedReference构造方法源码。
断点打在expectedReference == current.reference这行代码时,可以看到明明两个值都为2021,却比较得false。
原因:
Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实
例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;
可以自己测试一下: