CAS是什么
CAS又称 自旋锁、无锁,是一种乐观锁
- compare and swap 的缩写 意为: 比较并交换 , 实现并发算法的常用技术 , 就是说我不用加锁 , 也能保证 ( 加锁会影响效率,可以考虑使用原子操作类 ) 原子性 , 当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试 , 因此可以知道 , CAS只会允许一个线程的执行成功
- CAS 包括三个操作数 - 内存位置 , 预期原值以及更新值
- 执行 CAS 操作的时候,将内存位置的值与预期原值比较
- 如果相匹配 , 那么处理器会自动的将该位置更新为新值
- 如果不匹配 , 处理器不做任何操作 , 多个线程同时执行 CAS 的操作只有一个成功
- 执行 CAS 操作的时候,将内存位置的值与预期原值比较
例如下面的 AtomicInteger 等 原子类 就是基于 CAS 实现的
操作的过程图解:
- CAS有三个操作数,位置内存值V , 旧的预期值A , 要修改的更新值B
- 当且仅当旧的预期值A 和内存值V相同时 , 将内存值V修改为B , 否则什么都不做或重来
AtomicInteger 执行过程源码:
AtomicInteger atomicInteger=new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
// getAndIncrement方法 this是当前AtomicIntegr对象
// valueOffset是内存偏移量/值
// 1为自增量
// Unsafe类的getAndAddInt方法,该Unsafe类是在jdk里的rt.jar包
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//var1对象里的var2这个内存偏移量的值
var5 = this.getIntVolatile(var1, var2);
//var1 AtomicInteger 对象本身
//var2 该对象值得引用地址,内存地址偏移量
//var4 需要变动的数量 +1
//var5 是用var1 var2 找出的主内存中真实的值
//用该 对象当前的值与var5比较
//如果相同,进行var5+var4并且返回true
//如果不同,继续从主内存中取值然后 再比较,直到更新完成
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
创建一个原子类对象 ( 默认初始化值为0 ) , 也可以传入定值 , 当我们使用CAS方法时 , 传入一个预期值 , 一个修改值 , 仅当预期值和当前对象 valueOffset的内存偏移量的值相同 , 就可以修改成功 , 前提是没有被其他的线程抢先一步进行比较并交换 , 否则就要继续读取最新的修改值 *( 变量可以使用 volatile关键字解决可见性问题 , 就是说 : 一个线程对共享变量的修改,能够及时的被其他线程看到 , 立刻同步最新数值 )* , 再继续的进行比较并交换这一操作 , 直到修改成功..... 这是比较并交换的源代码 : native为本地方法 , 底层用C 实现对操作系统的访问和操作 (Java不可以直接访问)
*此代码以及执行流程如下 😗
*while ( ! this.compareAndSwapInt ( var1, var2, var5, var5 + var4 ) ) 的执行流程:*
- 当两个线程 A , B 同时执行 AtomicInteger 中的自增方法( getAndAddlnt ) 时 , 假如 AtomicInteger 中主内存的值是10
- 线程 A 调用 getlIntVolatile (var1, var2) 获取一份原始内存值(最新值)10 , 开始要操作了
- 线程 B 调用 getlIntVolatile (var1, var2) 也获取一份原始内存值(最新值)10 , 开始要操作了
- 线程 A 抢夺的速度很快, 调用 compareAndSwaplnt 方法 , 预期值和原始值相同都是10 , 比较并修改为11
- 线程 B 这时调用 compareAndSwaplnt 方法 , 发现自己的期望值是10,但是内存中真正的原始值被修改为11了,被其他线程修改了 , 这时线程A就修改失败 , 重新读取内存中的新值 value (volatile关键字作用: 及时的被其他线程看到最新的 value值 , 共享变量值的修改 )
- 线程 B 继续进行调用 compareAndSwaplnt 方法 , 比较并修改
*注意 : 这时线程B 还不一定就能修改成功 , 有可能还有其它线程抢夺的速度比它快 , 很快的将期望值 var5 进行了修改 , 线程B比较发现和期望值不匹配 , 因此线程B可能继续下去 , 直到比较成功 , 返回 ture 即可*
为什么原子类可以不加 synchronized 实现原子性 ?
何为原子性:原子就是最小的单位 , 不可分割的 , 就是指一个操作是不可中断的。即使是多个线程一起执行的时候 ,一个线程的操作一旦开始,执行的过程中不允许被中断 , 也就是说不会被其他线程干扰 , 直到执行完毕 , 不会造成数据不一致的问题 , 因此原子类使用 CAS 保证了并发情况下线程的安全性
\1. Unsafe 这个类是Java底层的一个类 , 是 CAS 的核心类 , 相当于是一个后门 , 它存在于 sun.misc 包中 , Unsafe 类中的所有的方法都是被 native 关键字修饰的 , 被 native 修饰的方法为本地方法 , 底层是用 C 实现对操作系统的访问和操作 , 就比如调用 UnSafe 类中的 CAS 方法 , 它是通过操作系统的汇编指令来实现操作内存数据的 , 因为Java中 CAS 操作的执行依赖于Unsafe 类的方法
\2. Unsafe 通过 对象的 valueOffset 内存偏移地址获取原始内存值 , 用当前工作内存值 (var1,var2) 和期望值 (var5) 进行比较判断是否能替换 , 一旦修改了 , 其他的线程一比较发现工作内存的值和期望值不一样 , 所以就无法操作了 , 只能继续获取新值 (getIntVolatile) , 继续比较 , 这样就保证了一次只能有一个线程在执行并且执行成功 , 就算不加锁 , 其他的线程也是进不来的 , 就无法操作 , 因此我们说 : 原子类可以不加 synchronized 就实现了原子性,保证了线程的安全
\3. 被 volatile 修饰的变量 , 保证了线程的可见性 , 一旦被修改 , 就能及时的将数据反馈给其他线程 , 以供其他线程获取最新的值
CAS 的缺点
- CAS 它是一条 CPU并发原语 , 当执行 getAndAddInt 方法的时候 , 里面有一个do while 方法 , 如果CAS失败 ,会一直进行尝试 比较 , 假如有一百万个线程 , 如果 CAS 长时间一直不成功 , 这么多的线程会直接给CPU打满 , CPU 的开销变大
- 当对一个共享变量执行操作时 , 我们可以使用循环 CAS 的方式保证原子操作 , 对多个共享变量操作时,循环CAS就无法保证操作的原子性 , 这个时候就可以用锁来保证原子性。
- ABA 问题 :
- 线程A从主内存中拿了一份值 , 假如是10 , 它在自己的工作空间中打算进行修改了 (还没修改)
- 这时线程B抢占了线程A , 它从主内存中将10取出来了, 它给修改成了22 , 放回主内存中了 , 但是呢 ,它感觉修改的不满意 , 又去主内存把那个22又取出来了 , 又给改成了10 , 然后走了
- 这时线程A 去进行比较并交换 , 发现工作内存值和主内存值(期望值)是一样的都是10 , 就成功修改了, 但是 ! 注意 ! 线程A并不知道这个值中途被其他线程修改了 ! ! !