1. 什么是CAS?
- CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
- CAS是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。
当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。 - 与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。
- 简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,哪说明它已经被别人修改过了。你就需要重新读取,再次尝试修改就好了。
2. CAS底层原理
这样归功于硬件指令集的发展,实际上,我们可以使用同步将这两个操作变成原子的,但是这么做就没有意义了。所以我们只能靠硬件来完成,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这类指令常用的有:
- 测试并设置(Tetst-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap)
- 加载链接/条件存储(Load-Linked/Store-Conditional)
2.1 CPU 实现原子指令有2种方式:
- 通过总线锁定来保证原子性。
总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享主内存。但是该方法成本太大(锁的粒度太大)。因此有了下面的方式。 - 通过缓存锁定来保证原子性。
在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把 CPU 和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化;
频繁使用的内存会缓存在处理器的 L1,L2 和 L3 高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在 LOCK 操作期间被锁定,当它执行锁操作回写主存时,处理器不在总线上声明 LOCK#信号,而是修改主存内部被修改数据的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止多个线程同时修改被两个以上处理器缓存的内存区域数据(即,每次只允许一个线程去修改被两个以上处理器缓存的内存区域数据),当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。
注意:有两种情况下处理器不会使用缓存锁定。
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
- 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。
2.2 CAS举例
public class AtomicDemo {
private AtomicInteger ai=new AtomicInteger(0);
private int i=0;
// 对原子类进行加一操作
public void increaseAtomic(){
ai.incrementAndGet();
}
// 对基本类型进行加一操作
public void increaseInt(){
i+=1;
}
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
atomicDemo.increaseAtomic();
atomicDemo.increaseInt();
}
}
2.3 CAS JVM指令分析
Compiled from "AtomicDemo.java"
public class com.lic.atomic.AtomicDemo {
public com.lic.atomic.AtomicDemo(); //AtomicDemo对象的构造函数
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #14 // class java/util/concurrent/atomic/AtomicInteger
8: dup // 复制栈顶部一个字长内容
9: iconst_0 // 将int类型常量0压入栈
10: invokespecial #16 // Method java/util/concurrent/atomic/AtomicInteger."<init>":(I)V
13: putfield #19 // Field ai:Ljava/util/concurrent/atomic/AtomicInteger;
16: aload_0
17: iconst_0
18: putfield #21 // Field i:I
21: return
public void increaseAtomic();
Code:
0: aload_0 // 从局部变量0中装载引用类型值
1: getfield #19 // 从对象中获取字段 --> Field ai:Ljava/util/concurrent/atomic/AtomicInteger;
4: invokevirtual #28 // 调度对象的实便方法 --> Method java/util/concurrent/atomic/AtomicInteger.incrementAndGet:()I
7: pop // 弹出栈顶端一个字长的内容
8: return
public void increaseInt();
Code:
0: aload_0 // 从局部变量0中装载引用类型值
1: dup // 复制栈顶部一个字长内容
2: getfield #21 // 从对象中获取字段 --> Field i:I
5: iconst_1 // 将int类型常量1压入栈
6: iadd // 执行int类型的加法
7: putfield #21 // 设置对象中字段的值 --> Field i:I
10: return
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/lic/atomic/AtomicDemo
3: dup
4: invokespecial #35 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #36 // Method increaseAtomic:()V
12: aload_1
13: invokevirtual #38 // Method increaseInt:()V
16: return
}
分析:
2.4 CAS源码分析
AtomicInteger.class源码(与示例程序无关代码已删除):
package java.util.concurrent.atomic;
public class AtomicInteger extends Number implements Serializable{
public AtomicInteger(int i){
value = i;
}
public AtomicInteger(){
}
//调用此方法进行加一操作
public final int incrementAndGet(){
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe; //Unsafe类,可以直接操作内存中的数据
private static final long valueOffset; //数据在内存中的地址偏移量
private volatile int value; //要修改的值, volatile保证可见性
static {
unsafe = Unsafe.getUnsafe(); //获取Unsafe类
try
{
valueOffset = unsafe.objectFieldOffset(java/util/concurrent/atomic/AtomicInteger.getDeclaredField("value"));
}
catch(Exception exception)
{
throw new Error(exception);
}
}
}
Unsafe.class源码(与示例程序无关代码已删除):
public final class Unsafe{
private static native void registerNatives();
private Unsafe(){
}
public static Unsafe getUnsafe()
{
Class class1 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(class1.getClassLoader()))
throw new SecurityException("Unsafe");
else
return theUnsafe;
}
// 参数分析:
// obj: 更新的AtomicInteger 对象
// l: atomicInteger对象的value的相对地址值, 在比较时通过该地址获取到value此时的值
// i: 期望值
// j: 置换值
//每次循环调本地方法时,传最新的预期值,和符合修改值。由本地方法中硬件层具体实现,如果预期值和最新值相同,将AtomicInteger对象的value值改为符合修改值
public final native boolean compareAndSwapInt(Object obj, long l, int i, int j);
//获取atomicInteger对象的value的值
public native int getIntVolatile(Object obj, long l);
public final int getAndAddInt(Object obj, long l, int i)
{
int j;
do
j = getIntVolatile(obj, l); //获取预期值
while(!compareAndSwapInt(obj, l, j, j + i)); //使用CAS实现加一的原子操作,操作失败后会不停重新尝试,直至操作成功
//该方法是先获取值,在进行增加操作,所以执行compareAndSwapInt方法后返回预期值,也就是修改之前的值
return j;
}
}
接下来看JVM中的unsafe.cpp
1. 所在目录: hotspot\src\share\vm\prims
2. 先在unsafe类中找到compareAndSwapInt类:
{CC"compareAndSwapInt", CC"(“OBJ"J”“I”“I”")Z", FN_PTR(Unsafe_CompareAndSwapInt)}
3. 找到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); //需要更新的atomicInteger对象
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // atomicInteger对象的value在内存中的具体位置 (offset是我们传递过来的value在内存中的地址偏移量)
// cmpxchg:汇编指令,用于比较并交换操作数,是一个原子操作
// e: 预期值
// x: 更新的值
// addr: 地址偏移量
//cmpxchg函数分析: 将e与addr地址指针指向的内容比较; 如果相同,则将x写入addr地址上并返回e; 如果不相等,则返回addr指向的内容
//如果写入成功则返回true,否则返回false
return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //(先强转,在与预期值e比较)
UNSAFE_END
4. Atomic类中关于cmpxchg函数的定义(了解):
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
assert(sizeof(jbyte) == 1, "assumption.");
uintptr_t dest_addr = (uintptr_t)dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
jint cur = *dest_int;
jbyte* cur_as_bytes = (jbyte*)(&cur);
jint new_val = cur;
jbyte* new_val_as_bytes = (jbyte*)(&new_val);
new_val_as_bytes[offset] = exchange_value;
while (cur_as_bytes[offset] == compare_value) {
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
return cur_as_bytes[offset];
}
3. CAS缺点
CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。
循环时间太长
如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
只能保证一个共享变量原子操作
看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高地位
ABA问题
CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
以上为自己理解,如果有分析错误的地方,欢迎指正!!!