JUC:CAS

JUC:CAS


关键词

  • CAS 的指令允许算法无锁的 执行原子操作的 读-修改-写 操作
  • CAS指令(线程数较少时性能上有提升)和 synchronized阻塞算法(性能优化非常好)
    1. cas,线程自旋的过程一直在cpu上跑,执行无用指令,没有从用户态切换到内核态(只是JVM在用户态的处理),开销因此变小
    2. synchronized,系统锁要从系统申请,线程状态切换,要经历用户态和内核态的切换(代价比较高),线程的重新唤醒也需要OS的支持
  • CAS底层原理:CPU并发原语,操作系统范畴,依赖硬件,不被中断
    1. 底层靠Unsafe类:保证原子性,调用getAndAddInt方法(Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断)
    2. 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底层原理

  1. Compare-And-Swap。是一条CPU并发原语。(原语:操作系统范畴,依赖硬件,不被中断。)
  2. 功能是判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
  3. 自旋:比较并交换,直到比较成功
  4. 底层靠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问题,循环时间长开销大和只能保证一个共享变量的原子操作

  1. ABA问题
    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成 1A - 2B-3A

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

AtomicStampedReference案例

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

  2. 只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用 循环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包中的高层类又是依赖于这些基础类来实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿城大饼

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值