写点什么

站在巨人肩上操作 CAS(一):CAS 的原理

用户头像
Android架构
关注
发布于: 10 小时前
  • 在某个线程执行完时间片之后,就会进行 CPU 切换;切换过程:清空寄存器和缓存数据,重新加载新的线程所需要的数据(线程私有的:JVM 运行时所需的数据:程序计数器、虚拟机栈和本地方法栈);

  • 此时之前的线程就被挂起,加入到阻塞队列中,在一定的时间和条件下,通过调用 notify() 或 notifyAll() 唤醒,进入就绪状态(runna


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


ble),等待 CPU 调度。


总结:


  • 总是假设最坏的情况,并且只有在确保其他线程不会造成干扰的情况下才会执行;

  • 导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁,再次进行锁的竞争(公平锁 / 非公平锁)。

缺点

  • 线程会被频繁的挂起,唤醒之后需要重新抢占锁

  • 场景:如果一个线程需要某个资源,但是这个资源的 占用时间很短 ,当线程第一次抢占这个资源时,可能这个资源被占用,如果此时挂起这个线程,但是可能立刻就发现资源已被刚才的线程的释放,然后又需要花费很长的时间重新抢占锁,时间代价就会非常的高。

乐观锁

思路

  • 每次 不加锁 而是假设 没有冲突 而去完成某种 操作,如果因为冲突 失败就重试 ,直到成功为止;

  • 当某个线程不让出 CPU 时,当前线程就一直 while 循环去尝试获取锁,如果失败就重试,直到成功为止;

  • 适用场景:当数据争用不严重时,乐观锁效果更好。

乐观锁应用

CAS


CAS(Compare and Swap)算法




非阻塞算法

核心参数

  • 主内存中存放的 V 值:线程共享(对应 JVM 的共享内存区:堆和方法区)

  • 线程上次从内存中读取的 V 值 A:线程私有,存放在线程的栈帧中

  • 需要写入内存中并改写 V 值的 B:线程对 A 值进行操作之后的值,想要存入主内存 V 中



  • V 值在主内存中保存,所有线程在对 V 值操作时,需要先从主存中读取 V 值到线程到工作内存 A 中;

  • 然后对 A 值操作之后,把结果保存在 B 中,最后再把 B 值赋给主内存的 V;

  • 多线程共用 V 值都是这样的过程;

  • 核心点:将 B 值赋给 V 之前,先比较 A 值和 V 值是否相等(判断,当前线程在计算过程中,是否有其他线程已经修改了 V 值),不相等表示此时的 V 值已经被其他线程改变,需要重新执行上面的过程;相等就将 B 值赋给 V。


如果没有 CAS 这样的机制,那在多线程的场景下,两个线程同时对共享数据进行修改,很容易就出错,导致某一个线程的修改被忽略。

核心代码

//原子操作


if (A == V) {


V = B;


return B;


} else {


return V;


}

变量 V

如何保证线程每次操作 V 时,都去主内存中获取 V 值?


  • 自然是在 CAS 的实现代码中,变量 V 是用 volatile 原语 修饰的:保证其可见性;


如何保证在进行 V 和 A 值比较相等之后,而在 B 值赋给 V 之前,V 值不会被其他线程所修改?


也就是说如何保证比较和替换这两个步骤的原子性?


看一下 CAS 原理:

CAS 原理

  • 底层的实现其实时通过调用 JNI(Java Native Interface 本地调用)的代码实现的;(其实 JNI 就是为了支持 Java 去调用其他语言)


具体看一下:AtomicInteger 类的 compareAndSet() 方法:


public final boolean compareAndSet(int expect, int update) {


return unsafe.compareAndSwapInt(this, valueOffset, expect, update);


}


调用的时 sun.misc 包下 Unsafe 类的 compareAndSwapInt()方法:(本地方法)


public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);


本地方法会调用 JDK 中的几个 cpp 文件(这是在 widowsX86 中的,不同操作系统调用的文件不同,文件中的实现方式也不同);


具体的 C++代码我就不贴了,知道上层的实现思路就行;


底层大致思路:通过对 cmpxchg 指令添加 lock 前缀(windowsX86 中的实现),确保对主内存中 V 值的 读-改-写操作原子执行;


不同的操作系统有不同的实现方式,但大致的思路是类似的,目的也都是保证比较赋值操作是原子操作。


更详细的底层实现,以及相关的一些关于 CPU 锁的内容,可以去看一下《Java 并发编程艺术》。


最终的实现作用:


  • 确保对主内存中 V 值的 读-改-写操作原子执行;

  • 禁止该指令与之前和之后的读(比较)和写(赋值)指令重排序。


CAS 缺点



ABA 问题

问题描述

CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。

解决方案

版本号

  • ABA 问题的解决思路就是使用版本号;

  • 在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A

AtomicStampedReference

compareAndSet() 方法源码:


public boolean compareAndSet(V expectedReference,


V newReference,


int expectedStamp,


int newStamp) {


Pair<V> current = pair;


return


expectedReference == current.reference &&


expectedStamp == current.stamp &&


((newReference == current.reference &&


newStamp == current.stamp) ||


casPair(current, Pair.of(newReference, newStamp)));


}


  • 跟其他的 Atomic 类不同的是, 这个多了一个引用的比较;

  • 这个会先检查 当前引用是否等于预期引用 ,并且当前标志是否等于预期标志,如果全部相等,则以 原子方式 将该 引用 和该标志的 设置为给定的更新值;

  • 所以如果有其他线程修改了, 那么引用就会改变, 当前线程就执行失败重试操作;

循环时间长

问题描述

因为 CAS 机制是: 一直进行失败重试, 直到成功为止, 那么自旋 CAS 如果长时间不成功, 就会给 CPU 带来非常大的执行开销;

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
站在巨人肩上操作CAS(一):CAS的原理