Fork me on GitHub

CAS理解

在谈CAS之前,我们谈谈java中的i++、i–这样的操作,我们知道它们不是线程安全的,因为类似的指令会被编译称为2条操作指令,在并发环境下,CPU在执行过程中可能会中断切换到别的线程,无法保证2条操作指令的原子性,所以是线程不安全的。
首先针对这种情况,我们可能第一反应就是利用synchronize关键字实现线程同步,保证++操作的原子性,但是我们今天来谈谈另一种解决办法,利用乐观锁的方式,这就不得不提到另一种选择–AtomicInteger。

查看源码:
首先我们看一下AtomicInteger类的类变量。可以看出有个unsafe的类变量。

1
2
3
4
5
6
7
8
9
10
11
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;

Unsafe类是用来在任意内存地址位置处读写数据,可见,对于普通用户来说,使用起来还是比较危险的。AtomicInteger类本质就是利用Unsafe.compareAndSwapInt这个CAS操作方法来保证Integer操作的原子性。

看一下AtomicInteger的对int的加法操作。

1
2
3
4
5
6
7
8
9
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

可以看出调用的是Unsafe类中的getAndAddInt方法,可以继续看看getAndAddInt的实现:

1
2
3
4
5
6
7
8
9
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);

与众不同的并发策略:比较交换(CAS)

与锁相比,使用CAS会使程序看起来复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远小于基于锁的方式。更重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有优越的性能。

CAS包含三个参数:CAS(V,E,N),V:要更新的变量,E:预期值,N:新值。当且仅当V的值与E的值相同时,才会将V的值设置为N,如果V值与E值不同,则说明已经有其他线程做了更新,当前线程什么都不做。

比如,现有t1、t2两个线程, t1和t2线程都同时去访问同一变量56,他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。
假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。

最后,CAS返回当前V的真实值。CAS是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时是用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的,如果变量不是你想象的那样,那说明它已经被别人修改过了,你就重新读取,再次尝试修改就好了。

钱聪 wechat
欢迎交流学习。
么么!