JUC:CAS
关键词
- CAS 的指令允许算法无锁的 执行原子操作的 读-修改-写 操作
- CAS指令(线程数较少时性能上有提升)和 synchronized阻塞算法(性能优化非常好)
- cas,线程自旋的过程一直在cpu上跑,执行无用指令,没有从用户态切换到内核态(只是JVM在用户态的处理),开销因此变小
- synchronized,系统锁要从系统申请,线程状态切换,要经历用户态和内核态的切换(代价比较高),线程的重新唤醒也需要OS的支持
- CAS底层原理:CPU并发原语,操作系统范畴,依赖硬件,不被中断
- 底层靠Unsafe类:保证原子性,调用getAndAddInt方法(Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断)
- while自旋:修改主内存
- 只能保证一个共享变量的原子操作 、循环时间长开销大、ABA问题
一、概述
1.1 概念
CAS,compare and swap的缩写,比较并交换。
compareAndSet方法:检测某线程的操作变量X没有被其他线程修改过(保证了线程安全)
CAS理论是它实现整个java cucurenct包的基石
CAS 操作包含三个操作数
- 内存位置(V)
- 预期原值(A)
- 新值(B)
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)
CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
类似于 CAS 的指令允许算法执行读-修改-写操作(原子操作),而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。
1.2 功能
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个JUC都是建立在CAS之上的。
- 判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
- cas有三个操作数,内存值V,旧预期值A,要更新的值B。仅当预期值A=内存值V时,才将内存值V修改为B,否则什么都不做。
synchronized阻塞算法效率低
JUC在性能上有了很大的提升
1.3 CAS底层原理
- Compare-And-Swap。是一条CPU并发原语。(原语:操作系统范畴,依赖硬件,不被中断。)
- 功能是判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
- 自旋:比较并交换,直到比较成功
- 底层靠Unsafe类保证原子性。
1.4 举例:AtomicInteger类的 getAndIncrement() 使用cas保证线程安全
- 调用了Unsafe类的getAndAddInt
- getAndAddInt使用cas一直循环尝试修改主内存
Unsave类
a. 该类所有方法都是native修饰,直接调用底层资源。sun.misc包中。
b. 可以像C的指针一样直接操作内存。java的CAS操作依赖Unsafe类的方法。
1.5 缺陷
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
-
ABA问题
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成 1A - 2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference(加时间戳)来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
AtomicStampedReference案例
暂略
-
循环时间长开销大(就一直do while尝试,CPU带来很大开销)
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 -
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用 循环CAS的方式来保证原子操作 ,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下 ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
1.4 JUC介绍
由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
- A线程写volatile变量,随后B线程读这个volatile变量
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量
volatile变量的读/写 和 CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile
- 然后,使用CAS的原子条件更新来实现线程之间的同步
- 同时,配合以volatile的读/写 和 CAS所具有的volatile读和写的 内存语义来实现线程之间的通信
AQS、非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。