CAS机制及AtomicInteger源码分析

一、CAS简介

CAS即Compare And Swap对比交换,区别于悲观锁,借助CAS可以实现区别于synchronized独占锁的一种乐观锁,被广泛应用在各大编程语言之中。Java JUC底层大量使用了CAS,可以说java.util.concurrent完全是建立在CAS之上的。但是CAS也有相应的缺点,诸如ABA、cpu使用率高等问题无法避免。

CAS总共有3个操作数,当前内存值V,旧的预期值A,要修改的新值N。当且仅当A和V相同时,将V修改为N,否则什么都不做。

二、CAS源码分析

我们都知道,java提供了一些列并发安全的原子操作类,如AtomicInteger、AtomicLong.下面我们拿AtomicInteger为例分析其源码实现。

// 1、获取UnSafe实例对象,用于对内存进行相关操作

private static final Unsafe unsafe = Unsafe.getUnsafe();

// 2、内存偏移量

private static final long valueOffset;

static {

try {

// 3、初始化地址偏移量

valueOffset = unsafe.objectFieldOffset

(AtomicInteger.class.getDeclaredField("value"));

} catch (Exception ex) { throw new Error(ex); }

}

// 4、具体值,使用volatile保证可见性

private volatile int value;

复制代码

从上面代码中我们可以看出,AtomicInteger中依赖于一个叫Unsafe的实例对象,我们都知道,java语言屏蔽了像C++那样直接操作内存的操作,程序员不需手动管理内存,但话说回来,java还是开放了一个叫Unsafe的类直接对内存进行操作,由其名字可以看出,使用Unsafe中的操作是不安全的,要小心谨慎。

valueOffset是对象的内存偏移地址,通过Unsafe对象进行初始化,有一点需要注意的是,对于给定的某个字段都会有相同的偏移量,同一类中的两个不同字段永远不会有相同的偏移量。也就是说,只要对象不死,这个偏移量就永远不会变,可以想象,CAS所依赖的第一个参数(内存地址值)正是通过这个地址偏移量进行获取的。

value属于共享资源,借助volatile保证内存可见性,关于volatile的简单分析,可以参考

Java 反汇编、反编译、volitale解读

// 1、获取并增加delta

public final int getAndAdd(int delta) {

return unsafe.getAndAddInt(this, valueOffset, delta);

}

// 2、加一

public final int incrementAndGet() {

return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

}

复制代码

上面两个方法依赖下面Unsafe类中的getAndAddInt操作,借助openjdk提供的Unsafe源码,我们看下其实现:

public final int getAndAddInt(Object o, long offset, int delta) {

int v;

// 1、不断的循环比较,直到CAS操作成功返回

do {

v = getIntVolatile(o, offset);

} while (!compareAndSwapInt(o, offset, v, v + delta));

return v;

}

复制代码

从上面可以看出,本质上CAS使用了自旋锁进行自旋,直到CAS操作成功,如果很长一段时间都没有操作成功,那么将一直自旋下去。

三、CAS的缺点

从第一、二节可以看出,CAS在java中的实现本质上是使用Unsafe类提供的方法获取对象的内存地址偏移量,进而通过自旋实现的。CAS的优点很明显,那就是区别于悲观策略,乐观策略在高并发下性能表现更好,当然CAS也是有缺点的,主要有类似ABA、自旋时间过长、只能保证一个共享变量原子操作三大问题,下面我们一一分析。

1、ABA

什么是ABA呢?简单的说,就是有两个线程,线程A和线程B,对于同一个变量X=0,A准备将X置为10,按照CAS的步骤,首先会从内存读取值旧的预期值0,然后比较,最后置为10,但就在A读取完X=0后,还没来得及比较和赋值,此时线程B完成了X=0 -> X=10 -> X=0这3个操作,随后A继续执行比较,发现此时内存的值依旧是0,最后CAS执行成功。虽然过程和结果没有问题,但是A比较时的0已经不是最初那个0了,有种被偷梁换柱的感觉。

下面代码举例演示ABA问题,线程1模拟将变量从100->110->100,线程2执行100->120,最后看下输出:

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicInteger;

import java.util.concurrent.atomic.AtomicStampedReference;

/**

*

* @Author jiawei huang

* @Since 2020年1月17日

* @Version 1.0

*/

public class ABATest {

// 初始值为100

private static AtomicInteger atomicInteger = new AtomicInteger(100);

public static void main(String[] args) throws InterruptedException {

// AtomicInteger实现 100->110->100

Thread t1 = new Thread(new Runnable() {

@Override

public void run() {

atomicInteger.compareAndSet(100, 110);

atomicInteger.compareAndSet(110, 100);

}

});

// 实现 100->120

Thread t2 = new Thread(new Runnable() {

@Override

public void run() {

try {

// 这里模拟线程1执行完毕,偷梁换柱成功

TimeUnit.SECONDS.sleep(2);

} catch (InterruptedException e) {

e.printStackTrace();

}

// 下面依旧返回true

System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100, 120));

}

});

t1.start();

t2.start();

}

}

复制代码

输出结果为:

AtomicInteger:true

复制代码

可见线程2中的CAS也执行成功了,那么如何解决这个问题呢?解决方案是通过版本号,Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

/*

* Copyright (C) 2011-2019 DL

*

* All right reserved.

*

* This software is the confidential and proprietary information of DL of China.

* ("Confidential Information"). You shall not disclose such Confidential

* Information and shall use it only in accordance with the argeements

* reached into with DL himself.

*

*/

package com.algorithm.leetcode.linkedlist;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicStampedReference;

/**

*

* @Author jiawei huang

* @Since 2020年1月17日

* @Version 1.0

*/

public class ABATest {

// 初始值100,版本号1

private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100, 1);

public static void main(String[] args) throws InterruptedException {

// AtomicStampedReference实现

Thread tsf1 = new Thread(new Runnable() {

@Override

public void run() {

try {

// 让 tsf2先获取stamp,导致预期时间戳不一致

TimeUnit.SECONDS.sleep(2);

} catch (InterruptedException e) {

e.printStackTrace();

}

// 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1

atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(),

atomicStampedReference.getStamp() + 1);

atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(),

atomicStampedReference.getStamp() + 1);

}

});

Thread tsf2 = new Thread(new Runnable() {

@Override

public void run() {

int stamp = atomicStampedReference.getStamp();

try {

TimeUnit.SECONDS.sleep(2); // 线程tsf1执行完

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(

"AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1));

}

});

tsf1.start();

tsf2.start();

}

}

复制代码

输出结果:

AtomicStampedReference:false

复制代码

可以看出线程1执行失败了。

2、自旋时间过长

通过第二节分析可以得知,CAS本质上是通过自旋来判断是否更新的,那么问题来了,如果多次旧预期值不等于内存值的情况,那么这个自旋将会自旋下去,而自旋过久将会导致CPU利用率变高。

3、只能保证一个共享变量原子操作

从第二节可以看出,只是单纯对单个共享对象进行CAS操作,保证了其更新获取的原子性,无法对多个共享变量同时进行原子操作。这是CAS的局限所在,但JDK提供同时了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200120A05INI00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券