1、CAS
CAS全称Compare And Swap,即比较并替换,是一种实现并发常用到的技术,Java并发包中有很多类用到的CAS技术,其核心算法思想如下:
执行函数:CAS(V,E,N)
其中包含3个参数:
- V表示内存地址
- E表示预期值
- N表示要更新的增量值(期望值)
CAS指令执行时,当且仅当内存地址V的值与预期值E相等时,将内存地址V的值修改为N,否则就什么都不做。整个比较并替换的操作是一个原子操作。
下面我们来看一段代码:
import java.util.ArrayList;
import java.util.List;
/**
* @Auther: yjntue
* @Date: 2020/3/2 21:50
* @Description:
*/
public class Test03 {
volatile int count = 0;
void m(){ //增加synchronized修饰符解决,同时可以注释掉volatile关键字
for(int i = 0; i<10000;i++){
count++;
}
}
public static void main(String[] args) {
Test03 t = new Test03();
List<Thread> threads = new ArrayList<Thread>();
for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
可以确定,无论count字段加不加volitile,都会出问题,预期是100000,实际不是。这是因为count++
是一个几个步骤的操作,无法保证这几个步骤的原子性的问题。
那如何改?只要加synchronized关键字即可,同时可以注释掉volatile。
/*volatile*/ int count = 0;
synchronized void m(){ //第一种方法:可以增加synchronized修饰符解决,同时可以注释掉volatile关键字
for(int i = 0; i<10000;i++){
count++;
}
}
或者改用java.util.concurrent.atomic.AtomicInteger
实现
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Auther: yjntue
* @Date: 2020/3/2 22:03
* @Description:
*/
public class Test04 {
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i = 0; i < 10000; i++){
count.incrementAndGet();
}
}
public static void main(String[] args) {
Test04 t = new Test04();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
为什么AtomicInteger可以?跟踪代码如下:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;//CAS修改成功后返回的最新值+1
}
再进行跟踪
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));//CAS修改成功后跳出循环
return var5;
}
可以看到接口的逻辑:
- 首先获取最新的v值(这个值就是预期值)
- 再传给 compareAndSwapInt 参数:预期值var5,新值var5+var4(增量值)等
- 如果所要修改的内存地址,其值等于预期var5,说明中途未被其他线程修改,则用新值替换该内存地址上的值(即旧值),修改成功将预期值(即旧值)返回,至此结束。
- 如果其值不等于预期var5,说明中途被修改了,则重复1、2、3直到成功为止
CAS的缺点
CAS虽然很高效的解决了原子性的问题,但是CAS依然存在三个问题:
- 循环时间长,开销大。
- 只能保证一个共享变量的原子操作。
- ABA问题
注意: 不断循环直至成功是sun.misc.Unsafe的API,不是CAS底层指令本身,底层的就操作一次,如果一次新值和预期不一样就不再尝试。是Unsafe的Java类自己实现的循环尝试,并且compareAndSwapXXX只尝试一次,只有getAndAddXXX和getAndSetXXX才实现了循环尝试。
循环实际长开销大
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
ABA 问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
参考:https://blog.youkuaiyun.com/w8y56f/article/details/89554154
https://blog.youkuaiyun.com/javazejian/article/details/72772470