CAS源码分析,以及ABA问题的解决

Angie ·
更新时间:2024-09-20
· 582 次阅读

CAS(比较并交换)一个小demo

import java.util.concurrent.atomic.AtomicInteger; public class CasDemo { public static void main(String[] args) { //默认初始值为5,也就是主存中的值为5 AtomicInteger atomicInteger = new AtomicInteger(5); //compareAndSet期望并交换,期望是第一个参数,期望拿走内存时和放回时实际内存值相同,更新值是更新到多少 System.out.println(atomicInteger.compareAndSet(5, 2048)); System.out.println("cas number:"+atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(5, 2222)); System.out.println("cas number:"+atomicInteger.get()); } }

在这里插入图片描述
第一次CAS是因为初始化AtomicInteger是5,所以交换成功(volatile关键字保证内存值实时同步可见)
第二次CAS中内存的值已被修改为2048所以无法修改返回false

下面是原子类CAS的源码

public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }

第一个参数为期望值也就是期望内存中的值为多少第二个参数为更新到多少
compareAndSwapInt方法源码,在Unsafe类中

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

其为native本地方法,不由Java实现
方法的作用就是,Object var1为当前对象,long var2为当前对象内存偏移量,var4为要进行的操作,var5为当前主内存地址的值

CAS底层原理
直接是对地址进行操作所以不需要加sync锁就可以保证安全性,CAS凭什么能保证原子性,靠的就是底层的unsafe类,Unsafe类是操作系统级别的操作,直接调用的是操作系统底层资源执行相应的任务
由原语所决定,原语默认是连续性的,不存在被中断 ,也就是说CAS是一条CPU原子指令,不会造成所谓的数据不一致问题

CAS自旋锁
加sync同一个时间段只能由一个线程来访问
一致性得到了保障但是并发性下降

cas一直循环,既提高了一致性又保证了并发性
以下就是CAS的应用:
AtomicInteger中的自增操作getAndIncrement(),其中
this是当前对象
ValueOffset是当前对象内存偏移量
i是加一的操作

public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }

Unsafe类中getAndAddInt()方法中的CAS自旋锁

public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }

第一个var5就是保存当前地址下的值(也就是主物理内存里的值) var5=this.getIntVolatile(var1,var2);(相当于先获取值在自增1(自增1的1就是var4))

whlie里面意思就是
比较并交换先检测var1这个对象和var2这个内存偏移量上的值是否与刚刚获取的var5相同,若相同则加var4也就是加1
var5是期望值
假如var1和var2的内存地址上的值跟期望值var5不相同,则while循环中this.compareAndSwapIntI(var1,var2,var5,var5 + var4)为假然后!号取反然后继续do内循环,再去找此内存地址上的值,再做比较,直到成功(也就是var5的值和var1var2对应的值相同时,这也是自旋锁的自旋之处)为止(这点会一直循环吗,只要不成功???疑惑)

疑惑解析!!!
do里面的var5会更新,因为此对象地址值var1var2已被volatile修饰,根据jmm内存可见所以就继续this.compareAndSwapIntI(var1,var2,var5,var5 + var4),然后直成功为止因为可能有很多线程在挂起时抢先执行

但是依然会造成一直匹配不到值的循环操作,这也是CAS的一个缺点

第二个缺点就是只能对一个对象进行原子操作,并不能对一块代码进行原子操作

第三个缺点就是接下来要讲的ABA问题
ABA问题的产生:
ABA问题就是加入主存中的值为A
(1)两个线程都会去主存里拿到一份值A
(2)t1线程由于执行时间短,两秒,所以执行了又A->B->A的操作
(3)由于JMM原理,主存中也是同样执行了这样的操作
(4)线程t2在执行完之后看到主存中的值还是A就进行交换,但其实已经被修改过了,又改回来了
在这里插入图片描述
解决:
加上一个时间戳,每改变一次,自增一次,相当于版本号
使用原子引用类AtomicStampedReference

public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); }

参数一还是对象,参数二则是一个初始版本号
然后每次改变主存中的值就会改变版本号,这样就有效 避免了ABA问题的发生
解决ABA问题demo如下

package com.wsx.aba; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class SloveAba { static AtomicReference atomicReference = new AtomicReference(100); static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1); public static void main(String[] args) { System.out.println("ABA问题的产生"); new Thread(()->{ atomicReference.compareAndSet(100,101); atomicReference.compareAndSet(101,100); System.out.println(Thread.currentThread().getName()+"\t"+atomicReference.get()); },"t1").start(); new Thread(()->{ try { //为了让t1完成一次ABA操作 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicReference.compareAndSet(100,2019); System.out.println(Thread.currentThread().getName()+"\t"+atomicReference.get()); },"t2").start(); //主线程睡眠等待上限操作完成 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ABA问题的解决"); new Thread(()->{ int stamp = atomicStampedReference.getStamp(); //这里睡一秒是为了t3线程和t4线程的起点一致,版本号都为1 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t最新版本号"+atomicStampedReference.getStamp()); //ABA带版本号操作 atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t最新版本号"+atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t最新版本号"+atomicStampedReference.getStamp()); },"t3").start(); new Thread(()->{ int stamp = atomicStampedReference.getStamp(); //这里睡两秒是为了让t3线程完成ABA操作 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } //这里的stamp是默认没被修改的版本号,如果被其他线程修改过那么这里版本号就开始落后,然后修改失败 boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); System.out.println(Thread.currentThread().getName()+"\t最新版本号"+atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName()+"\t是否改动成功"+result); System.out.println(Thread.currentThread().getName()+"\t最新值"+atomicStampedReference.getReference()); },"t4").start(); } }
作者:一只程序熊



cas

需要 登录 后方可回复, 如果你还没有账号请 注册新账号