CAS即compareAndSwap,比较并交换
如原子型整数,AtomicInterger中就有一个CAS方法(CAS在底层,此Set的S非彼S,不要误解,往下看!):
compareAndSet(expect,update),输入一个期望值expect,和一个更新值update,
如果atomicInterger=expect,则重新赋值为update,并返回true。
如果值已经被改动,则返回false,不进行操作。
底层是使用Unsafe类,jre的rt.jar中sum/misc目录下。
里面的方法全部都是用native修饰的本地方法,可以直接使用系统资源进行操作,是最高级的,不可被打断,所以保证了其原子性!
而这个AtomiciInterger中的数据就是用volatile修饰的,保证了其可见性!
所以通过CAS+volatile这种方法也可以保证JMM模型的三大特性!
但是CAS也是有缺点的,还一下子就是三个!
CAS三大缺点
1.耗时长,消耗系统资源
因为是CAS需要进行比较,在一些操作中,如AtomicInterger中的getAndIncrement()方法
其源码调用的就是Unsafe的getAndAddInt()方法
而再里面就是用的compareAndSwap这种真正的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;
}
举个例子解释一下
AtomicInteger atomicInteger = new AtomicInteger();//默认值为0
如拿到的是atomicInterger的值=5,现在要继续加1
var1就是 atomicInterger这个对象,var2就是偏移量地址,
unsafe就是通过这个偏移量地址去内存找出它的值为5;
用var5来接收从内存读到的值;
var4就是我们需要加的值,这里是+1;
开始进行CAS,期望值为var5,更新值为var5+var4;
本地方法开始操作,可以当做打开了一个绝对领域,这里只有我在操作,无法被打断或操作!
在里面一样通过var1和var2获得一个值用var3来接收
当var3==var5时,我们就将值改为var4,输入到内存中!
但前面在外面获取值时,还没进入绝对领域的情况下,别人已经改完了,内存里的值已经是6了!
因为用volatile修饰过,所以进入领域中就可以看到已经变成6了!
那么比较的结果就为false,只能退出领域重新获取值再进入!
所以这就是为什么要写一个dowhile循环,这样就有可能进入自旋状态,一直判断一直判断!
就很浪费资源和时间!但也是因为这样才保证了其原子性!不像普通的线程不管三七二十一,一顿乱改!一点规矩都不讲!
2.只能有一个共享变量
原子数据类型,一次只能使一个共享变量拥有原子性!
3.ABA
什么是ABA?
举个例子!
有线程1和线程2同时从内存中读到了值为A,
这个时候线程1在对A进行操作时,线程2已经将A改成B并放入到内存中,
但是线程3这个时候跑过来把你B又改成了A再放回去。
此时线程1才到绝对领域中进行CAS,比较了一下发现,值没错,就是A,我开始操作巴拉巴拉!
虽然线程1使用CAS保证了原子性,但中间被人偷偷改过都不知道,所以可能造成一定概率的错误!
public class AtomicDemo {
public static void main(String[] args) {
AtomicReference<String> atomicStr = new AtomicReference<String>("A");
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//目标值,更新值,目标版本,更新版本
atomicStr.compareAndSet("A","C");
System.out.println(Thread.currentThread().getName()+"\t "+atomicStr.get());
},"1").start();
new Thread(()->{
//目标值,更新值,目标版本,更新版本
atomicStr.compareAndSet("A","B");
atomicStr.compareAndSet("B","A");
},"2").start();
}
}
输入结果如下
1 C
怎么避免ABA?
最简单的方法就是打个标记,如时间戳版本号之类的。
原子类不止有基本的数据类型,还有原子引用!可以将我们自定义的类原子化!
原子引用AtomicReference
AtomicReference<Integer> integerAtomicReference = new AtomicReference<>();
使用泛型的方式包裹任意类,可以是基本数据类型,也可以是我们的自定义类。
不过要解决ABA的问题,光用这个不够,这个类只能保证原子性,我们还要为这个类打上记号stamped这个类就叫做AtomicStampedReference
/**
* Creates a new {@code AtomicStampedReference} with the given
* initial values.
*
* @param initialRef the initial reference
* @param initialStamp the initial stamp
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
其构造方法不仅需要一个保护对象,还要打入一个int类型的记号,我们可以简单理解为版本号!
public class AtomicDemo {
public static void main(String[] args) {
AtomicStampedReference<String> atomicStr = new AtomicStampedReference<>("A", 1);
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//目标值,更新值,目标版本,更新版本
atomicStr.compareAndSet("A","C",1,2);
System.out.println(Thread.currentThread().getName()+"\t "+atomicStr.getReference()+"\t"+atomicStr.getStamp());
},"1").start();
new Thread(()->{
//目标值,更新值,目标版本,更新版本
atomicStr.compareAndSet("A","B",1,2);
atomicStr.compareAndSet("B","A",2,3);
},"2").start();
}
}
1 A 3
当我们再遇到ABA问题时,就可以先比较其版本号,值和版本号都匹配成功,我们才进行修改,否则不操作!