CAS 与原子操作
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CAS 与原子操作相关的知识,希望对你有一定的参考价值。
参考技术A锁可以从不同的角度分类。其中,乐观锁和悲观锁是一种分类方式。
乐观锁:
乐观锁又称为“无锁”。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为 CAS 的技术来保证线程执行的安全性。
由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说 乐观锁免疫死锁 。
乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。
悲观锁:
悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
在Java中可以通过锁和循环 CAS 的方式来实现原子操作。
CAS 的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
比较并交换的过程如下:
CAS 指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
我们以一个简单的例子来解释这个过程:
在这个例子中,i就是V,5就是A,6就是B。
那有没有可能我在判断了 i 为5之后,正准备更新它的新值的时候,被其它线程更改了 i 的值呢?
不会的。因为CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环 CAS 就是在一个循环里不断的做 cas 操作,直到成功为止。
CAS 是怎么实现线程的安全呢?
我们将其交给硬件 — CPU 和内存,利用 CPU 的多处理能力,实现硬件层面的阻塞,再加上 volatile 变量的特性即可实现基于原子操作的线程安全。
CAS 是一种无锁算法,通过硬件层面上对先后操作内存的线程进行排队处理,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS(比较并交换)是CPU指令级的操作, 只有一步原子操作,所以非常快 。而且避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了
1、ABA 问题
CAS 在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个 版本号 ,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在 jdk 中提供了 AtomicStampedReference 类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用。
这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前版本号标志是否等于预期版本号标志,如果二者都相等,才使用CAS设置为新的值和标志。
2、循环时间长开销大
自旋 CAS 如果长时间不成功,会占用大量的 CPU 资源,给 CPU 带来非常大的执行开销。
解决思路是让 JVM 支持处理器提供的 pause 指令 。
pause 指令 能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的CPU流水线重排的代价也会小很多。
3、只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比 如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。
解决方案:
AtomicInteger
实例:
打印结果:12。
ai.compareAndSet(10, 12); 改为 ai.compareAndSet(11, 12);时,打印结果:10
AtomicIntegerArray
主要是提供原子的方式更新数组里的整型,其常用方法如下。
需要注意的是,数组 value 通过构造方法传递进去,然后 AtomicIntegerArray 会将当前数组复制一份,所以当 AtomicIntegerArray 对内部的数组元素进行修改时,不会影响传入的数组。
实例
打印结果:
原子更新基本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下 3 个类。
**AtomicReference **
原子更新引用类型。
实例:
打印结果:
AtomicStampedReference
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在 ABA 问题了。这就是 AtomicStampedReference 的解决方案。AtomicMarkableReference 跟 AtomicStampedReference 差不多,AtomicStampedReference 是使用 pair 的 int stamp 作为计数器使用,AtomicMarkableReference 的 pair 使用的是 boolean mark。 AtomicStampedReference 可能关心的是动过几次, AtomicMarkableReference 关心的是有没有被人动过,方法都比较简单。
实例:
打印结果:
AtomicMarkableReference
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)。
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类, Atomic 包提供了以下 3 个类进行原子字段更新。 要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类, 每次使用的时候必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用 public volatile 修饰符。
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。
Java并发编程系列- 原子操作与CAS
3. 原子操作与CAS
3.1 原子操作
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch,也就是切换到另一个线程。
为了实现原子操作,Java中可以通过synchronized关键字将函数或者代码块包围,以实现操作的原子性。但是synchronized关键字有一些很显著的问题:
1、synchronized是基于阻塞锁的机制,如果被阻塞的线程优先级很高,可能很长时间其他线程都没有机会运行;
2、拿到锁的线程一直不释放锁,可能导致其他线程一直等待;
3、线程数量很多时,可能带来大量的竞争,消耗cpu,同时带来死锁或者其他安全。
像synchronized这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正好可以进行某项操作,如果要是发生冲突呢,那我就重试直到成功,乐观锁最常见的就是CAS。
JAVA内部在实现原子操作的类时都应用到了CAS。
3.2 CAS
CAS是CompareAndSwap的缩写,即比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作,大多数现代处理器都支持CAS指令。
JDK中关于原子操作的类有,
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
下面例子为AtomicInteger的基本用法,
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());//10--->11
System.out.println(ai.incrementAndGet());//11--->12--->out
System.out.println(ai.get());
}
}
深入getAndIncrement的实现可以发现,内部调用了CAS进行value的更新,
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
CAS在底层通过JNI调用系统的Atomic::cmpxchg方法,来进行value的比较与更新。
对于CAS的实现,这依赖于CPU提供的特定指令,具体根据体系结构的不同还存在着明显区别。比如,x86 CPU 提供 cmpxchg 指令;而在精简指令集的体系架构中,则通常是靠一对儿指令(如“load and reserve”和“store conditional”)实现的,在大多数处理器上 CAS 都是个非常轻量级的操作,这也是其优势所在。
3.3 CAS的问题
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- ABA问题
什么是ABA问题?ABA问题怎么解决?如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。
Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
- 开销问题
循环时间长开销很大:我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
本文由『后端精进之路』原创,首发于博客 http://teckee.github.io/ , 转载请注明出处
搜索『后端精进之路』关注公众号,立刻获取最新文章和价值2000元的BATJ精品面试课程。
以上是关于CAS 与原子操作的主要内容,如果未能解决你的问题,请参考以下文章