CAS实现原子操作的三大问题
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger atomicInteger = new AtomicInteger(0);
private int i=0;
// 非线程安全方法
private void count(){
i++;
}
// 线程安全方法
private void safeCount(){
for(;;){
int i = atomicInteger.get();
boolean succ = atomicInteger.compareAndSet(i,++i);
if(succ)
break;
}
}
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for(int j=0;j<100;j++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<1000;i++){
//分别调用安全方法和非安全方法
cas.count();
// cas.safeCount();
}
}
});
ts.add(t);
}
for(int i=0;i<ts.size();i++)
ts.get(i).start();
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicInteger.get());
}
}
分别调用安全方法和非安全方法,可以发现,调用非安全方法返回的值大部分情况下不到100000,而使用安全方法返回的值每次都准确为100000。
但是使用CAS实现原子操作可能带来三个问题
- ABA问题。因为CAS在比操作值得时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,中间变成B,最后又变成A,那么使用CAS进行检查时会发现值没有发生变化,但是实际上是变化了的。解决思路就是添加版本号。 JDK1.5之后,Atomic包提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public boolean weakCompareAndSet(
V expectedReference, // 预期引用
V newReference, // 新引用
int expectedStamp, // 预期标志
int newStamp) { // 新标志
return compareAndSet(expectedReference, newReference,expectedStamp, newStamp);
}
- 循环时间开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作。当对多个共享变量CAS操作不能保证原子性,可以把多个共享变量合并成一个共享变量来操作。或者使用加锁的方式。