1. CAS
在多线程编程时,如果想保证一段代码具有原子性,通过会使用锁来解决,而CAS是通过硬件指令来达到比较并交换的过程;
CAS原理
CAS包括三个值:
V:内存地址;
A:期望值;
B:新值;
如果这个内存地址V的值和期望值A相等,则将其赋值为B;
2 CAS存在的问题
2.1 ABA问题
在多线程并发场景下:线程A、B、C 同时对资源 R=1进行修改,线程A期望将R修改为2,在修改之前,线程B拿到CPU资源将R修改为3,线程B结束后,线程C拿到CPU资源将R修改为1,此时线程A接着去做修改,成功了,在线程A看来,R=1一直没有被修改,所以造成ABA问题;
staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量,在 CAS 操作静态属性时,会用到这个偏移量。
objectFieldOffset 方法用于获取非静态 Field (非静态属性)在 Object 实例中的偏移量,在 CAS 操作对象的非静态属性时,会用到这个偏移量。问题复现:*
public class CASTest {
private static Unsafe unsafe;
private static long valueOffset;
static {
Field theUnsafe = null;
try {
theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
valueOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("value"));
} catch (Exception e) {
e.printStackTrace();
}
}
private volatile int value=1;
public static void main(String[] args) throws Exception{
final CASTest casTest = new CASTest();
casTest.testABA(casTest);
}
private void testABA(final CASTest casTest){
final Thread threadB = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程B:"+casTest.value);
int expect = 1;
int update = 3;
System.out.println("准备更新时线程B:"+casTest.value);
System.out.println("线程B:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
}
});
final Thread threadC = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程C:"+casTest.value);
try {
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
int expect = 3;
int update = 1;
System.out.println("准备更新时线程C:"+casTest.value);
System.out.println("线程C:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
}
});
final Thread threadA = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程A:"+casTest.value);
try {
threadC.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
int expect = 1;
int update = 2;
System.out.println("准备更新时线程A:"+casTest.value);
System.out.println("线程A:"+unsafe.compareAndSwapInt(casTest, valueOffset, expect, update));
}
});
threadA.start();
threadC.start();
threadB.start();
}
}
输出结果:
开始前线程A:1
开始前线程C:1
开始前线程B:1
准备更新时线程B:1
线程B:true
准备更新时线程C:3
线程C:true
准备更新时线程A:1
线程A:true
通过控制线程执行顺序,复现此问题,但是我们发现如果是对于数值类型的ABA问题不是问题;
来试一下compareAndSwapObject,同样让它产生ABA问题,看是否有影响:
public class CASTest {
private static Unsafe unsafe;
private static long valueOffset;
static {
Field theUnsafe = null;
try {
theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
valueOffset = unsafe.objectFieldOffset(CASTest.class.getDeclaredField("value"));
} catch (Exception e) {
e.printStackTrace();
}
}
private volatile Student value= new Student("");
public static void main(String[] args) throws Exception{
final CASTest casTest = new CASTest();
casTest.testABA(casTest);
}
private void testABA(final CASTest casTest){
final Thread threadB = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程B:"+casTest.value);
System.out.println("准备更新时线程B:"+casTest.value);
System.out.println("线程B:"+
unsafe.compareAndSwapObject(casTest,
valueOffset,
casTest.value,
new Student("tom")));
}
});
final Thread threadC = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程C:"+casTest.value);
Student expect = casTest.value;
try {
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("准备更新时线程C:"+casTest.value);
System.out.println("线程C:"+unsafe.compareAndSwapObject(
casTest,
valueOffset,
casTest.value,
new Student("")));
}
});
final Thread threadA = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程A:"+casTest.value);
try {
threadC.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("准备更新时线程A:"+casTest.value);
System.out.println("线程A:"+unsafe.compareAndSwapObject(
casTest,
valueOffset,
casTest.value,
new Student("ThreadA")));
}
});
threadA.start();
threadC.start();
threadB.start();
}
}
class Student{
private String name;
public Student(String name){
this.name=name;
}
}
开始前线程A:com.didichuxing.erp.srmsync.juc.Student@6fe706ec
开始前线程C:com.didichuxing.erp.srmsync.juc.Student@6fe706ec
开始前线程B:com.didichuxing.erp.srmsync.juc.Student@6fe706ec
准备更新时线程B:com.didichuxing.erp.srmsync.juc.Student@6fe706ec
线程B:true
准备更新时线程C:com.didichuxing.erp.srmsync.juc.Student@40d974e1
线程C:true
准备更新时线程A:com.didichuxing.erp.srmsync.juc.Student@6120f560
线程A:true
可以看到Student实例已经发生了多次变化,volatile Student value
属于多线程内存可见,当其他线程对value做了修改,另外的线程是能获取到最新值的,去做compareAndSwapObject
是成功的,但如果这样:;
final Thread threadA = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程A:"+casTest.value);
Student student = casTest.value;
try {
threadC.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("准备更新时线程A:"+casTest.value);
System.out.println("线程A:"+unsafe.compareAndSwapObject(
casTest,
valueOffset,
student,
new Student("ThreadA")));
}
});
结果肯定是false,那既然是false,引用类型会产生ABA问题吗?个人理解,我觉得引用类型不会产生ABA问题,但如果另外一个线程对value的age字段做了修改,会不会产生问题,我们试一下:
final Thread threadC = new Thread(new Runnable() {
public void run() {
casTest.value.age=1;
}
});
final Thread threadA = new Thread(new Runnable() {
public void run() {
System.out.println("开始前线程A:"+casTest.value);
Student student = casTest.value;
try {
threadC.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("准备更新时线程A:"+casTest.value);
System.out.println("线程A:"+unsafe.compareAndSwapObject(
casTest,
valueOffset,
student,
new Student("ThreadA")));
}
});
开始前线程A:{name:,age:0}
准备更新时线程A:{name:,age:1}
线程A:true
这倒是产生了另外一个问题,如果预期和内存中的student是同一个地址,但是成员变量已经产生了变化,同样更新成功了;
2.2 ABA问题解决办法
总结一下上面提到的ABA问题:
- 数值类型ABA问题;
- 引用类型对象成员变量被改变时,与内存中的不一致,任然能够修改成功;
ABA问题解决:
Integer initValue = 100;
Integer initVersion = 1;
final AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(initValue,initVersion);
final Thread threadA = new Thread(new Runnable() {
public void run() {
int stamp = reference.getStamp();
System.out.println("线程A:" +
reference.compareAndSet(100, 200, stamp, stamp + 1));
}
});
final Thread threadB = new Thread(new Runnable() {
public void run() {
int stamp = reference.getStamp();
try {
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B:" +
reference.compareAndSet(200, 300, stamp, stamp + 1));
}
});
threadB.start();
threadA.start();
AtomicStampedReference 原理:
public class AtomicStampedReference<V> {
// 定义引用类型,包装值和版本号;
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
// 比较并交换
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
// 先做一次校验,如果在这里都已经不一致,则直接返回false,这里没有加锁,那么它可能会存在并发;
// 可能会有两个线程同时进来,判断并且都成立,则两个线程都会进入到:casPair方法;
// Pair<V> current = pair; 多个线程进入到compareAndSet方法时,都已经保留了当前的pair值,那如果pair被其他线程修改,则另外一个线程去做cas的时候一定会返回false,所以这块是通过这种方式来防止并发的;
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
String field, Class<?> klazz) {
try {
return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
} catch (NoSuchFieldException e) {
// Convert Exception to corresponding Error
NoSuchFieldError error = new NoSuchFieldError(field);
error.initCause(e);
throw error;
}
}
}
2.3 只能保证一个共享变量的原子操作
至于这个问题,可以参考:AtomicStampedReference
将多个变量封装成对象,再对对象做CAS;
2.4 循环时间长开销大
关于这个问题,我觉得应该还好吧,可以根据服务器配置查询MIPS指标,CPU每秒执行指令数是远远超过CAS的并发;