前言
CAS
(Compare and Swap
),即比较并替换
,实现并发算法时常用到的一种技术,java同步器中大量使用了CAS技术,实现了多线程执行的安全性。
CAS的思想
:三个参数,一个当前内存值V
、旧的预期值A
、即将更新的值B
,当且仅当预期值A和内存值V相同
时,将内存值
修改为B
并返回true
,否则什么都不做
,并返回false
public class Case {
public volatile int n;
public void add() {
n++;
}
}
通过javap -verbose Case看看add方法的字节码指令
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
LineNumberTable:
line 6: 0
line 7: 10
}
n++
被拆分成了几个指令
:
- 执行getfield拿到原始n;
- 执行iadd进行加1操作;
- 执行putfield写把累加后的值写回n;
通过volatile
修饰的变量
可以保证线程
之间的可见性
,但并不能保证这3个指令
的原子执行
,在多线程并发执行下
,无法做到线程安全
,得到正确的结果,那么应该如何解决呢?
如何解决
- 在add方法加上synchronized修饰解决
public class Case {
public volatile int n;
public synchronized void add() {
n++;
}
}
这个方案当然可行,但是性能上差了点
看一段代码
public int a = 1;
public boolean compareAndSwapInt(int b) {
if (a == 1) {
a = b;
return true;
}
return false;
}
对应字节码指令如下
public boolean compareAndSwapInt(int);
descriptor: (I)Z
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #2 // Field a:I
4: iconst_1
5: if_icmpne 15
8: aload_0
9: iload_1
10: putfield #2 // Field a:I
13: iconst_1
14: ireturn
15: iconst_0
16: ireturn
LineNumberTable:
line 11: 0
line 12: 8
line 13: 13
line 15: 15
StackMapTable: number_of_entries = 1
frame_type = 15 /* same */
如果这段代码在并发下执行,会发生什么?
假设线程1和线程2都过了a==1的检测,都准备执行对a进行赋值,结果就是两个线程同时修改了变量a,显然这种结果是无法符合预期的,无法确定a的最终值。
解决方法也同样暴力,在compareAndSwapInt
方法加锁同步
,变成一个原子操作
,同一时刻只有一个线程才能修改变量a。
除了低性能的加锁方案,我们还可以使用JDK自带的CAS方案,在CAS中,比较和替换是一组原子操作
,不会被外部打断
,且在性能上更占有优势。
下面以AtomicInteger的实现为例,分析一下CAS是如何实现的
public class AtomicInteger extends Number implements java.io.Serializable {
// 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;
public final int get() {return value;}
}
Unsafe
,是CAS的核心类
,由于Java方法无法直接访问底层系统,需要通过本地(native
)方法来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存的数据
变量valueOffset,表示该变量值在内存中的偏移地址
,因为Unsafe
就是根据内存偏移地址
获取数据
的。
变量value用volatile
修饰,保证了多线程之间的内存可见性
看看AtomicInteger
如何实现并发下的累加
操作:
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//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;
}
假设线程A和线程B同时执行
getAndAdd
操作(分别跑在不同CPU上
):
AtomicInteger
里面的value
原始值为3
,即主内存中AtomicInteger
的value
为3,根据Java内存模型
,线程A
和线程B
各自持有一份value的副本
,值为3
线程A通过
getIntVolatile(var1, var2)
拿到value
值3
,这时线程A被挂起
。线程B也通过
getIntVolatile(var1, var2)
方法获取到value
值3,运气好,线程B没有被挂起,并执行compareAndSwapInt
方法比较内存值
也为3,成功修改内存值
为2
这时线程A恢复,执行
compareAndSwapInt
方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程
提前修改过了,那只能重新来一遍了。重新获取value值,因为变量
value
被volatile
修饰,所以其它线程对它的修改,线程A总是能够看到
,线程A继续执行compareAndSwapInt
进行比较替换,直到成功
整个过程中,利用CAS保证了对于value的修改的并发安全,继续深入看看Unsafe
类中的compareAndSwapInt
方法实现
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
先想办法拿到变量value在内存中的地址。
通过
Atomic::cmpxchg
实现比较替换,其中参数x是即将更新的值,参数e是原内存的值intel手册对lock前缀的说明如下:
确保后续指令执行的原子性
- 在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
2.禁止该指令与前面和后面的读写指令重排序。
3.把写缓冲区的所有数据刷新到内存中。
上面的第2点和第3点所具有的内存屏障
效果,保证了CAS
同时具有volatile读
和volatile写
的内存语义
CAS缺点
CAS存在一个很明显的问题,即ABA问题
问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记
的原子引用类
AtomicStampedReference,它可以通过
控制变量值的版本来保证
CAS的正确性`
转自
深入浅出CAS