CAS,即Compare-And-Swap比较并交换,它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则改为新的值,这个过程是原子的
通过AtomInteger
举例:
public class CasDemo {
public static void main(String[] args) {
compareAndSet();
}
private static void compareAndSet() {
AtomicInteger integer = new AtomicInteger(4);
for (int i = 0; i < 5; i++) {
final int n = i;
new Thread(() -> {
// 比较并设置,如果主内存中的值与期望值相同,则设置新值并返回true,否则返回false
boolean result = integer.compareAndSet(4, n);
System.out.println(
Thread.currentThread().getName() + " result: " + result + ", current value: " + integer.get());
}, "thread-" + i).start();
}
}
}
结果:
thread-0 result: true, current value: 0
thread-3 result: false, current value: 0
thread-4 result: false, current value: 0
thread-2 result: false, current value: 0
thread-1 result: false, current value: 0
可见只有一个线程设置新值成功,多个线程对同一变量的操作具有原子性
compareAndSet
方法是如何做到在多线程环境下,安全的对变量比较并设置值的呢?
通过查看源码我们得知
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
方法调用的是unsafe.compareAndSwapInt
方法,而unsafe
这个对象又是什么呢?
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
看到AtomInteger
类有这么几个变量:
Unsafe unsafe
long valueOffset
volatile int value
Unsafe
类存在于rt.jar
中的sun.misc
包中,是CAS的核心类,由于Java无法直接访问操作系统底层,需要通过本地方法(native)来访问,Unsafe
类就是Java访问系统底层的一种方式,可以直接操作特定内存中的数据
valueOffset
表示该变量在内存中的内存偏移地址,Unsafe
类就是通过内存偏移地址获取数据
value
是变量的值,利用volatile
修饰后保证了多线程间内存的可见性
CAS并发原语体现在Java语言中就是sum.misc.Unsafe
类中的各个方法,调用Unsafe
类中的方法时,JVM会实现CAS汇编指令,调用Unsafe
类的方法实际就是执行一套CPU原子指令,是不允许被终端的,不会造成数据不一致的问题
再来看AtomInteger
的compareAndSet
方法
// AtomInteger
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
可以看到AtomInteger
的compareAndSet
方法调用的是Unsafe
的compareAndSwapInt
方法
// Unsafe
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
而Unsafe
的compareAndSwapInt
方法是一个本地方法,大致就是对比var1
对象的var2
地址的数据和期望值var4
,如果相等就修改为var5
CAS的缺点
-
长时间循环操作的开销大
看一下
Unsafe
类中的getAndAddInt
方法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; }
当CAS失败时,会循环尝试,如果CAS长时间不成功,可能会给CPU带来性能开销
-
只能保证一个共享变量的原子操作
当对一个共享变量进行操作时,我么你可以使用循环CAS的方式保证原子操作,但当对多个共享变量操作时,循环CAS无法保证操作的原子性,只能用锁来保证
会引出ABA问题
ABA问题
当T1、T2两个线程同时访问一个变量时,两线程同时取出变量N为A,假设T2执行比T1快,T2修改值为B,然后再将其修改为A,此时T1线程进行CAS发现内存中值仍然是A,然后更新至成功
尽管T1线程操作执行成功,但是不代表过程没有问题
先来看一下原子引用(AtomicReference)
public class AtomicReferenceDemo {
public static void main(String[] args) {
Person p1 = new Person("p1", 10);
Person p2 = new Person("p2", 20);
AtomicReference atomicReference = new AtomicReference<>();
atomicReference.set(p1);
System.out.println(atomicReference.compareAndSet(p1, p2) + " ref: " + atomicReference.get());
System.out.println(atomicReference.compareAndSet(p1, p2) + " ref: " + atomicReference.get());
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
结果:
true ref: Person{name='p2', age=20}
false ref: Person{name='p2', age=20}
原子引用就是将自定义的引用类型变为原子的引用类型,实现对引用类型数据的原子操作
与其他类一样,原子引用也会带来ABA问题
public class AbaDemo {
private static AtomicReference atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
atomicRef();
}
private static void atomicRef() {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "thread-1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 200) + " ref: " + atomicReference.get());
}, "thread-2").start();
}
}
结果:
true ref: 200
我们可以通过对原子引用变量添加一个版本号,通过比较版本号是否相同来判断数据是否被修改过,这就是AtomicStampedReference
类
public class AbaDemo {
private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
atomicStampedRef();
}
private static void atomicRef() {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "thread-1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 200) + " ref: " + atomicReference.get());
}, "thread-2").start();
}
private static void atomicStampedRef() {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " stamp-1: " + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " stamp-2: " + stamp);
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " stamp-3: " + stamp);
}, "thread-1").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " stamp: " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean res = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1);
stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " res:" + res + " stamp: " + stamp);
System.out.println(Thread.currentThread().getName() + " ref:" + atomicStampedReference.getReference());
}, "thread-2").start();
}
}
结果:
thread-1 stamp-1: 0
thread-2 stamp: 0
thread-1 stamp-2: 1
thread-1 stamp-3: 2
thread-2 res:false stamp: 2
thread-2 ref:100
可以看到两个线程第一次获取stamp均为0,然后thread-1进行了一次ABA操作,之后stamp(也就是stamp-3)变为了3,在当thread-2按照之前获取的版本号去更新数据时,虽然数据值仍然为100但是版本号已经变更,所以更新失败