什么是CAS
CAS 的全称是 Compare And Swap 即比较交换,其算法核心思想如下函数:CAS(V,E,N) 参数:
- V 表示要更新的变量
- E 预期值
- N 新值
如果 V 值等于 E 值,则将 V 的值设为 N。若 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
通俗的理解就是 CAS 操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行 CAS 操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。
我们看一下例子:
- 在内存地址V当中,存储这值为10的变量
2. 此时线程1想要把变量的值增加1。对于线程1来说,旧的预期值为E=10,要修改的值 N=11。
3. 线程1要提交更新之前,另一个线程2抢先一步,把内存地址V的变量值先更改成了11
4. 线程1开始提交更新,首先进行E和内存V中实际值比较,发现E不等于V的实际值,提交失败。
5. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值,此时对线程1来说,E=11,N=12。这个重新尝试的过程被称为自旋。
6. 这一次没有发现其他线程改变地址V的值。线程1进行Compare ,发现N和地址V的实际值是相等的。
7.线程1进行swap,把地址V的值替换为N ,也就是12.
CAS 举例说明
public static int count = 0;
public static void main(String[] args) {
//
for (int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
}catch (Exception e){}
//每个线程当中让count值自增100次
for(int j=0;j< 10000;j++){
count++;
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){}
System.out.println("count=="+count);
}
如上示例程序,因为上面代码不是线程安全的,所以最终的结果可能会小于 50000。
那么怎么解决呢?可以加锁(synchronized)。
public static int count = 0;
public static void main(String[] args) {
//
for (int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
}catch (Exception e){}
//每个线程当中让count值自增100次
for(int j=0;j< 10000;j++){
synchronized (Demo1.class) {
count++;
}
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){}
System.out.println("count=="+count);
}
加了同步锁以后,count自增的操作变成了原子性操作,所以最终的输出一定是count = 50000。
Synchronized 的确保证了线程安全,但是在某些情况下,却不是一个最优选择。
为什么这么说?关键在于性能问题
Synchronized 关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLED状态,这个过程中设计到的操作系统用户模式和内核模式,代价比较高。
尽管jdk1.6 为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低
java原子操作类,指的是java.util.concurrent.atomic包下,一些列以Atomic开头的包装类。例如AtomicBoolean、AtomicInteger、AtomicLong。他们分别用于boolean、Integer、Long类型的原子性操作。
现在我们尝试在代码中引入**AtomicInteger **类:
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
//
for (int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
}catch (Exception e){}
//每个线程当中让count值自增100次
for(int j=0;j< 10000;j++){
synchronized (Demo1.class) {
count.incrementAndGet();
}
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){}
System.out.println("count=="+count.get());
}
使用AtomicInteger之后,最终的输出结果同样可以保证是50000。并且在某些情况下,代码的性能会比Synchronized更好。
Atomic 操作类的底层,正是利用了CAS机制;
CAS 底层实现
下面看一下AtomicInteger的源代码
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
private volatile int value;
public final int get() {
return value;
}
这段代码是一个无限循环,也就是CAS的自旋。循环体当中做了三件事:
- 获取当前值
- 当前值+1,计算出目标值
- 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
这里需要注意的重点是get方法,这个方法的作用是获取变量的当前值。
如何保证获得的当前值时内存中的最新值呢?很简单,用volatile关键字来保证,
可是compareAndSet方法是如何保证原子性操作的呢??
接下来看一看compareAndSet方法的实现,以及方法所依赖对象的来历:
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。
**什么是unsafe呢?**Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。
而unsafe的compareAndSwapInt方法参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
CAS缺陷
1.ABA 问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
2.循环时间长 开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
2.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子
性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作