学习多线程的时候看到很多人说CAS,并且在concurrent包里面到处都是CAS,就稍微去学习了一下。稍微做个总结
简介:
CAS: Compare and Swap
直接翻译就是 比较并交换。
直接从字面上理解就会发现我们已经有了疑问:
- 比较,和什么比较,或者谁和谁比较。
- 交换,和什么交换,或者谁和谁交换。
Compare and Swap 简析
先找一个使用到这个算法的类入手来学习,这里选择AtomicInteger
类,找到其中一个比较常见的方法incrementAndGet
来进行分析,下面先给出调用的方法,
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//Unsafe 类中的方法 Unsafe类中几乎都是native的方法。
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;
}
//最终的调用方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//与之类似的还有 compareAndSwapLong compareAndSwapObject
incrementAndGet
作用时是将当前的值 +1 并返回结果。可以看到其实CAS操作在这个是对值进行修改的,那么这个比较应该就是在值之间进行比较了。再分析方法之前我们还需要关注一下看AtomicInteger
类中的一些默认的参数和构造函数,这样做便于追踪最终调用的方法compareAndSwapInt
中的各个参数。
// 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;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
几个细节需要注意:
- value是由volatile修饰的,volatile的作用时保证变量内存可见性,也就是内存中其他线程可以读写这个参数。在这里的
incrementAndGet
方法操作的就是这个变量。 - valueOffset:这里指的就是value这个属性在内存中的偏移量(内存中的地址,而不是值),当类被加载时先按顺序初始化static变量和static块,通过unsafe中的
public native long objectFieldOffset(Field var1);
获取AtomicInteger
类属性value在内存中的偏移量,并将偏移量值赋给valueOffset。所以这个属性的值是value属性在内存中的偏移量。
对于incrementAndGet
方法中逻辑是先调用的getAndAddInt
方法,在其最终的结果 + 1 之后返回,也就是说实际上getAndAddInt
应该是对内存进行了操作,但是返回值本身并不是操作之后的结果。继续看getAndAddInt
参数从字面上直接理解,第一个参数是this 也就是当前的对象本身,第二个参数是偏移量,第三个为固定数值 1 。 getAndAddInt 源码中先调用了getIntVolatile
方法,这方法,我源码中没有注释,在别的地方找到了一个注释:
/***
* Retrieves the value of the integer field at the specified offset in the
* supplied object with volatile load semantics.
*
* @param obj the object containing the field to read.
* @param offset the offset of the integer field within <code>obj</code>.
*/
public native int getIntVolatile(Object obj, long offset);
大概的意思就是获取obj对象中offset偏移地址对应的整型field的值,并且支持volatile load语义。volatile的使用保证了即使此时数据被修改,也是对当前线程可见的。整个方法简单理解就是取offset所能确定的整型值。
取到值之后,进入后续的关键调用compareAndSwapInt
,继续找注释。
/***
* Compares the value of the integer field at the specified offset
* in the supplied object with the given expected value, and updates
* it if they match. The operation of this method should be atomic,
* thus providing an uninterruptible way of updating an integer field.
*
* @param obj the object containing the field to modify.
* @param offset the offset of the integer field within <code>obj</code>.
* @param expect the expected value of the field.
* @param update the new value of the field if it equals <code>expect</code>.
* @return true if the field was changed.
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
这个方法其实字面意思就能理解,expect就是预期的值,update就是要更新的值。
解读注释:比较从obj对象指定偏移量的地方读出来的整型数与传入的预期值进行对比,假如两者相匹配,则将对应地址的值更新为传入的update的值。这个操作必须具有原子性,所以这里提供了一个不能被打断的方法。
这里带入数据就是obj为this即当前对象,offset为指定的偏移量,expect是从offset偏移量中取出的整型数,update为取出来的数+1之后的结果。
看到这里就很清晰了,原先的问题谁和谁比,就是对象的指定偏移量中取出的指定类型的值与传入的预期值进行比较。而交换也很明确可以看出来是原有的值和需要更新的值进行交换。
compareAndSwapInt 为什么需要原子性
其实很容易分析出来,假如方法运行到了取值出来并且比较通过了准备将新的数据写进去的时候了,此时又有另一个线程对同一个偏移量的值进行修改,并且也通过了比较的操作。这个时候两次修改,先修改的必将被后修改的覆盖,最终造成的结果可能就是值只是增加了1 而预期的应该增加2 所以这里必须是原子操作,ABA 问题
假设有两个线程T1、T2,同时操作队形O:- T1读取数据得到A,
- 此时T1被挂起,T2执行。
- T2 读取数据也是A,并且执行,将其修改为B
- T2 继续操作,在此以同样的操作将B又改为A
- 此时T2被挂起,T1继续执行
- T1 获取到数据此时还是A(修改过之后)
- T1 继续将数据修改。
虽然从结果上看,并没有问题但是从过程上看并不是预期的,并且是存在安全隐患的。
ABA问题我们可以使用JDK的并发包中的AtomicStampedReference
和AtomicMarkableReference
来解决,AtomicStampedReference
和AtomicMarkableReference
是通过版本号(时间戳)来解决ABA问题的,我们也可以使用版本号(verison)来解决ABA。即乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。