CAS,compare and swap 比较和替换是使用的一个期望值和当前值进行比价,如果当前值与期望值相等,就使用新值替换当前值。
原子操作
假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
CAS实现原子操作的三大问题
1) ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
2) 循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3) 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
Jdk中相关原子操作类的使用
AtomicInteger
方法 | 返回类型 | 描述 |
---|---|---|
getAndIncrement() | int | 以原子方式将当前值加1 返回加1之前的值 |
incrementAndGet() | int | 以原子方式将当前值加1 返回加1之后的值 |
getAndAdd(int delta) | int | 以原子方式将输入的数值与实例中的值相加 返回加delta之前的值 |
addAndGet(int delta) | int | 以原子方式将输入的数值与实例中的值相加 返回加delta之后的值 |
compareAndSet(int expect, int update) | boolean | 如果currentValue 等于 expectValue,就设置为updateValue,否则不变 |
getAndSet(int newValue) | int | 以原子方式设置为newValue的值,并返回旧值 |
public class AtomTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
int a = atomicInteger.getAndIncrement(); // 以原子方式将当前值加1 返回加1之前的值 10
int b = atomicInteger.incrementAndGet(); // 以原子方式将当前值加1 返回加1之后的值 12
int c = atomicInteger.getAndAdd(10); // 以原子方式将输入的数值与实例中的值相加 返回加10之前的值 12
int d = atomicInteger.addAndGet(10); // 以原子方式将输入的数值与实例中的值相加 返回加10之后的值 32
// 如果currentValue 等于 expectValue,就设置为updateValue,否则不变
boolean flag = atomicInteger.compareAndSet(32, 200); // true
System.out.println("flag:"+flag+", atomicInteger:"+atomicInteger); // true 200
flag = atomicInteger.compareAndSet(300, 400); // false
System.out.println("flag:"+flag+", atomicInteger:"+atomicInteger); // false 200
int e = atomicInteger.getAndSet(9); // 返回旧值 200
System.out.println(e);
}
}
AtomicIntegerArray
方法 | 返回类型 | 描述 |
---|---|---|
addAndGet(int i, int delta) | int | 以原子方式将输入值delta与数组中索引 i 的元素相加,返回相加后的值 |
compareAndSet(int i, int expect, int update) | boolean | 如果当前索引 i 的值 等于预期值expect,则以原子方式将数组位置i的元素设置成update值 |
注: 数组通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。
public class AtomTest {
public static void main(String[] args) {
int[] array = new int[]{100,200,300};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
int g = atomicIntegerArray.addAndGet(0, 1000); // 以原子方式将输入值delta与数组中索引 i 的元素相加,返回更新后的值
System.out.println(g); // 1100
int j = atomicIntegerArray.get(0);
System.out.println(j); // 1100
System.out.println(Arrays.toString(array)); // [100, 200, 300]
boolean flag2 = atomicIntegerArray.compareAndSet(1, 200, 2000); // 如果当前索引 i的值 等于预期值expect,则以原子方式将数组位置i的元素设置成update值
System.out.println(flag2); // true
int k = atomicIntegerArray.get(1);
System.out.println(k); // 2000
System.out.println(Arrays.toString(array)); // [100, 200, 300]
}
}
更新引用类型
AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类
AtomicReference 原子更新引用类
public class AtomTest {
static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
UserInfo user = new UserInfo("zhangsan", 15);//要修改的实体的实例
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("liSi",17);
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get()); // UserInfo{name='liSi', age=17}
System.out.println(user); // UserInfo{name='zhangsan', age=15}
}
//定义一个实体类
static class UserInfo {
private volatile String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题。这就是AtomicStampedReference的解决方案。
AtomicMarkableReference跟AtomicStampedReference差不多,AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。
public class AtomTest {
static AtomicStampedReference<String> asr = new AtomicStampedReference("oldRef",0);
public static void main(String[] args) throws InterruptedException {
//拿到当前的版本号(旧)
final int oldStamp = asr.getStamp();
final String oldReference = asr.getReference();
System.out.println(oldReference+"============"+oldStamp); // oldRef============0
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":当前变量值:" +oldReference + "-当前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReference, "newRef1", oldStamp, oldStamp + 1));
// Thread-0:当前变量值:oldRef-当前版本戳:0-true
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName() +":当前变量值:" +reference + "-当前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference, "newRef2", oldStamp, oldStamp + 1));
// Thread-1:当前变量值:newRef1-当前版本戳:1-false
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference()+"============"+asr.getStamp()); // newRef1============1
}
}
AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)
参考享学