CAS原理分析

107 篇文章 0 订阅

CAS机制是为了实现原子性操作的,是实现原子操作的其中一种方式,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的数据与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。现代的大多数CPU都实现了CAS,它是一种无锁(lock-free),且非阻塞的实现机制,用来保持数据的一致性。

在JDK 5之前Java语言是靠synchronized关键字保证同步的,使用的是锁机制。

锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

在多线程同步问题中,为了避免使用锁机制,volatile是一种不错的机制,但是volatile不能保证原子性。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突的去完成某项操作,如果因为冲突导致失败了就重试,直到成功为止。乐观锁用到的机制就是CAS。

一、什么是CAS

CAS,compare and swap的缩写,中文翻译成:比较并交换。

我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量支持并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。

在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需考虑其他线程可能同时修改该变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。

CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。这一比较并交换的操作是原子的,不可以被中断。初一看,CAS也包含了读取、比较 (这也是种操作)和写入这三个操作,和之前的i++并没有太大区别,是的,的确在操作上没有区别,但CAS是通过硬件指令保证了原子性,而i++没有,且硬件级别的原子性比i++这样高级语言的软件级别的运行速度要快地多。虽然CAS也包含了多个操作,但其运算是固定的(就是个比较运算),这样的性能开销很小。

从内存领域来说这是乐观锁,因为它在对共享变量更新之前会先比较当前值是否与更新前的值一致,如果是,则更新,如果不是,则无限循环执行(称为自旋),直到当前值与更新前的值一致为止,才执行更新。

简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当内存值V和预期值A相同时,才将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。后面会看一下AtomicInteger是如何利用CAS实现原子性操作的。

二、CAS的目的

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。

其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此相对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

三、CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。
1.ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

2.循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3.只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

四、 concurrent包的实现

由于java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

五、CAS在AtomicInteger中应用

下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。

通过查看AtomicInteger的源码可知, 通过申明一个volatile类型的变量,再加上unsafe.compareAndSwapInt的方法,来保证实现线程同步的。

volatile变量:

private volatile int value;  

首先声明了一个volatile变量value, 我们知道volatile保证了变量的内存可见性,也就是所有工作线程中同一时刻都可以得到一致的值。
在这里插入图片描述
Compare And Set:

//java.util.concurrent.atomic包下的原子操作类都是基于CAS实现的,接下去我们通过AtomicInteger来看看是如何通过CAS实现原子操作的:

// 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"));// 反射出value属性,获取其在内存中的位置    
	} catch (Exception ex) { 
		throw new Error(ex); 
	}    
}    

// 可见性,为了在多线程环境中能看到最新的值,保证内存中的数据值一旦发生改变都能被其他线程看到,然后再进行cas操作。
private volatile int value;
public final int get() {return value;}
    
public final boolean compareAndSet(int expect, int update) {    
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);    
}  

比较并设置,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证"读-改-写"是一个原子操作

compareAndSwapInt有4个参数:

  • this - 当前AtomicInteger对象,
  • Offset - value属性在内存中的位置(需要强调的是,不是value值在内存中的位置),
  • expect - 预期值,
  • update - 新值,

核心点:

  1. Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
  2. valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。
  3. value是用volatile修饰的,保证了多线程之间看到的value值是同一份,是最新值。

根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。

新版本的AtomicInteger的源码可能略有变化:

private volatile int value;

/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(int expect, int update) {
    return U.compareAndSwapInt(this, VALUE, expect, update);
}

六、incrementAndGet循环设置

现在在来看开篇提到的两个方法:


public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
 
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

我们拿incrementAndGet来分析一下其实现过程。

public final int incrementAndGet() {    
    for (;;) {	// 这样优于while(true)    
        int current = get();// 获取当前值    
        int next = current + 1;// 设置更新值    
        if (compareAndSet(current, next))    
            return next;    
    }    
}   

循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新值,否则重试到成功为止。

七、incrementAndSet

CAS是在底层硬件指令上实现的原子操作,中间是不可以被打断的。
(1)先读取当前值,假设i=0
(2)i++
(3)尝试设置i=1:先对比预期值是否为0,如果为0则设置i=1;


cas(v, expected, newValue)
 
即:
if(v == expected)
    v = newValue;

假设线程1与线程2同时调用incrementAndSet方法,只有一个线程能成功完成CAS。

CAS在底层的硬件级别保证操作一定是原子的(即先比较再设置是原子操作),只有一个线程可以成功执行CAS,其他线程去执行CAS时会失败。假设线程1成功完成CAS,线程2在执行CAS过程中发现值不对,则CAS执行会失败,会重新读取当前值,再次尝试CAS。

八、CAS底层原理

jvm中的CAS操作最终是在硬件层基于处理器的CMPXCHG指令实现的。CMPXCHG指令是实现比较并交换的原子操作。

核心是Unsafe类(在jdk中rt.jar中的sun.misc下的Unsafe类),由于Java 不能直接访问操作系统底层,需要通过本地方法(native方法)来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。换句话说就是CAS其实是直接操作内存地址中的内容。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被打断,也就是说CAS是依赖于CPU原子指令的操作,不会造成所谓的数据不一致问题。

Unsafe类

Java 不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe 类提供了硬件级别的原子操作。Unsafe 类在 sun.misc 包下,不属于 Java 标准。很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发,比如 Netty、Hadoop、Kafka 等。

(1) Unsafe 是用于在实质上扩展 Java 语言表达能力、便于在更高层(Java 层)代码里实现原本要在更低层(C 层)实现的核心库功能用的。
(2) 这些功能包括裸内存的申请/释放/访问,低层硬件的 atomic/volatile 支持,创建未初始化对象等。
(3) 它原本的设计就只应该被标准库使用,因此不建议在生产环境中使用。

九、总结:

可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。

参考:
CAS操作确保原子性
CAS原子性操作
什么是乐观锁,什么是悲观锁

JAVA面试题——CAS原理
一文彻底搞懂CAS实现原理 & 深入到CPU指令
Java内存模型JMM学习之CAS(依赖硬件的CPU原子指令保证原子性)
CAS && Unsafe的使用
原子类型的使用&Unsafe&CAS
Java多线程之五:CAS操作

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值