一》对于CAS的理解

要对CAS进行探究,我们先从AtomicInteger这个类的getAndIncrement()这个方法说起 ,这个方法主要可以解决volatile关键字不保证原子性的问题。下面我们进入这个方法中进行进行探究:

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

进入getAndIncrement()这个方法,可以看到底层调用了Unsafe这个类对象的getAndAddInt()方法,再进入Unsafe这个类:

 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);public native Object getObjectVolatile(Object var1, long var2);public native void putObjectVolatile(Object var1, long var2, Object var4);

Unsafe这个类来自JVM里rt.jar这个包,是JVM自身携带的一个类,可以看到里边的方法都是native关键字修饰的,说明Unsafe这个类的方法都是调用操作系统底层的资源来执行相应的任务的,类似于C语言中的指针操作内存。getAndIncrement()这个方法之所以能保证原子性,就是Unsafe这个类起到了作用。

再进入getAndAddInt(this, valueOffset, 1)这个方法:

 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;}

这段代码就是CAS操作的核心,可以看到主要是通过循环来实现的,先来看看几个核心参数:

var1:表示当前的对象;

var2 :表示地址偏移量(Unsafe就是通过地址偏移量来获取数据的);

var5 :表示主物理内存中真实的共享变量的值;

var4+var5:表示变化量

首先通过var1和var2来获取主物理内存中的真实值var5,再与当前对象的值进行比较,如果相等,更新var4+var5,返回true,跳出while循环,如果不相等,继续进行取值和比较,直到相等为止。

既然Unsafe这个类是通过偏移量进行获取数据的,那么这个偏移量是怎么计算的呢?

 static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;

可以看到底层的value值用volatile关键字修饰,这样就保证了共享变量的可见性。

讲到这里,那么什么是CAS呢,所谓的CAS,就是一个比较并交换的过程,它是通过自旋锁的方式进行线程安全的保证。它先将预期值和真实值进行比较,看是否相等,相等的话,就进行修改,这个过程是原子性的。CAS是一条并发原语,是CPU的原子指令,依赖于硬件的物理功能,执行过程不会被打断,就不会存在数据不一致的问题。

讲到这里,大家可能还是不太懂,下面就通过一个具体的实例来体会CAS的原理:

  public static void main(String[] args) {AtomicInteger atomicInteger=new AtomicInteger(5);//先将主物理内存中的5改为2019boolean b = atomicInteger.compareAndSet(5, 2019);//再将主内存中的值改回5boolean b1 = atomicInteger.compareAndSet(5, 5);System.out.println(b+"\t"+atomicInteger.get());System.out.println(b1+"\t"+atomicInteger.get());}

运行结果:

主物理内存里的共享变量的值为5,调用compareAndSet()方法进行比较并交换,通过比较,主物理内存中的值和期望的值是相等的,这时候交换成功,返回true,主物理内存中的值被换成2019。但是当重新改回5的时候,返回是false,说明修改失败,主内存中的值依然是2019,下面通过图示的方法分析一下这个修改失败的原因。

先来对这个JMM内存模型进行一个描述:

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

第一次修改之后,主内存中的共享变量的真实值变成了2019,当第二次想改回来的时候,将期望值5和主内存中的值2019进行比较,发现二者不相等,所以修改失败。

二》CAS的缺点

CAS虽然能保证线程安全性和并发性,但还有缺点:

1.自旋的时间过长,消耗资源;

2.只能保证一个共享变量的原子性;

3.会产生ABA问题

三》ABA问题

ABA问题就是,一个线程修改了主物理内存中的值,又将值改为原来的值,这时候另一个线程来进行比较,以为值没有被修改,但实际被修改过,类似于狸猫换太子。

比如线程T1执行时间需要10s,线程T2执行时间是2s,线程T2比线程T1快,加入主物理内存中的共享变量的值是A,T2经过比较,跟自己的工作内存中的值是一致的,这时候就把值改为B并放到主物理内存中,由于T2执行速度快,这时候又把值改为A,这时T1线程执行,自己工作内存中的值和主物理内存中的值相同,以为没被改过,但实际上被动过,类似于狸猫换太子

在学习ABA问题之前,我们先来了解一下原子引用。

在前面我们提到了原子整形AtomicInteger,它放进主物理内存的值只能是整形,但是当我们想把自定义对象放进主物理内存,就要用到原子引用。

 public static void main(String[] args) {User user=new User(1,"zhangsan");AtomicReference<User> atomicReference=new AtomicReference<>();//将user对象放进了主物理内存atomicReference.set(user);System.out.println(atomicReference.get());

运行结果:

可以看到,通过原子引用,我们成功的把自定义的对象放进了主物理内存。

那么ABA问题该怎么解决呢?这里我们需要用到版本号原子引用AtomicStampedReference。

定义一个版本号,版本号的作用就是,当线程对主物理内存中的值改变时,每改变一次,就让版本号加1,当发生ABA问题,另一个线程进行比较的时候,虽然主物理内存中的值和期望值一致,但是版本号不一致,这时,就不能成功修改,防止了ABA问题。

下面看一个实例:

   //版本号默认是1,主内存的值是100AtomicStampedReference<Integer> atomicStampedReference=newAtomicStampedReference<>(100,1);//创建线程anew Thread(()->{int tamp=atomicStampedReference.getStamp();//初始版本号System.out.println(Thread.currentThread().getName()+"第一次版本号为: "+tamp);//暂停一秒,让b线程读取初始版本号try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//让a线程进行一次ABA操作atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"第二次版本号为: "+atomicStampedReference.getStamp());//在将2019改回100,版本号+1atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"第三次版本号为: "+atomicStampedReference.getStamp());},"a").start();//创建线程bnew Thread(()->{int tamp=atomicStampedReference.getStamp();//初始版本号//暂停4秒,让a线程进行一次ABA操作try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"第一次版本号为: "+tamp);//b线程进行值的修改,版本号+1atomicStampedReference.compareAndSet(100,2019,tamp,atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName()+"第二次版本号为: "+atomicStampedReference.getStamp());System.out.println(atomicStampedReference.getReference());},"b").start();}

运行结果:

b线程的期望版本号是2,因为修改了一次,但是a线程进行ABA操作,修改了两次,实际版本号是3,版本号不相同,b线程修改值失败,依然是100。

深度理解CAS和ABA问题相关推荐

  1. 无锁数据结构--理解CAS、ABA、环形数组

    在分布式系统中经常会使用到共享内存,然后多个进程并行读写同一块共享内存,这样就会造成并发冲突的问题, 一般的常规做法是加锁,但是锁对性能的影响非常大. 无锁队列是一个非常经典的并行计算数据结构,它极大 ...

  2. 高并发之深度解析CAS [理论+案例+源码]

    目录 1.理论 2.问题 3.实操--Atomic打头的类 4.原理:基于类Striped64分散热点[空间换时间] 1.理论 CAS,即compare and swap,比较并交换,包含三个操作数, ...

  3. atomic原子类实现机制_并发编程:并发操作原子类Atomic以及CAS的ABA问题

    本文基于JDK1.8 Atomic原子类 原子类是具有原子操作特征的类. 原子类存在于java.util.concurrent.atmic包下. 根据操作的数据类型,原子类可以分为以下几类. 基本类型 ...

  4. java购买同一件商品时加锁_java中CAS的ABA问题思考和整理(不看后悔系列)

    前言 听说经常面试被问到~今天同事说了这个问题,就查了一下这问题,觉得挺有意思的,就整理出来跟大家分享下.主要思考下面几个问题: 1.什么是CAS? 2.什么是CAS的ABA问题? 3.怎么解决这个问 ...

  5. 沈向洋:从深度学习到深度理解

    2020-07-30 01:29:20 作者 | 蒋宝尚 编辑 | 丛 末 7月19日,深圳市人工智能与机器人研究院与香港中文大学(深圳)联合主办的"全球人工智能与机器人前沿研讨会" ...

  6. 深度理解do{} while(0)语句的作用

    深度理解do{} while(0) 在linux内核中常常会看到do{} while(0)这样的语句,有人疑惑,认为无意义,因为他只执行一次,加不加do{} while(0)小过失完全一样的,那你就错 ...

  7. CAS的ABA问题描述 AtomicStampReference

    CAS的ABA问题描述 在CAS操作的时候,其他线程将当前变量的值从A改成B,又改回A: CAS线程用期望值A与当前变量比较的时候,发现当前变量没有变,于是CAS就将当前变量进行了交换操作,但其实当前 ...

  8. go之树型结构深度理解补充

    go之树型结构深度理解补充 在上一篇中借用了 Ilija Eftimov 文章来讲解了tree的定义和一些方法.这篇文章主要是讲解在树型结构中如何判断节点与节点之间的关系. A节点是否是B节点的直接上 ...

  9. 【TensorFlow】TensorFlow从浅入深系列之二 -- 教你通过思维导图深度理解深层神经网络

    本文是<TensorFlow从浅入深>系列之第2篇 TensorFlow从浅入深系列之一 -- 教你如何设置学习率(指数衰减法) TensorFlow从浅入深系列之二 -- 教你通过思维导 ...

最新文章

  1. java c 序列化_Java 序列化
  2. 计算机动画制作 实验要求,有关计算机动画设计课程教学的对比试验
  3. dataframe保存为txt_如何批量查找并修改替换 Word、PPT、Excel、PDF、TXT等文件的内容...
  4. windows下使用pthread库
  5. css提取页面元素唯一性_下面这个函数,能够获取一个元素的任意 CSS 属性值。...
  6. 如何避免mysql回表查询_mysql如何避免回表查询
  7. Jupyter notebook中怎么添加Pytorch运行环境
  8. 严肃贴:内幕 手机行业
  9. android 日历_适用于Android的十大最佳日历应用
  10. react 点击使父元素消失_在 React 组件中使用 Refs 指南
  11. win8蓝屏错误代码DPC_WATCHDOG_VIOLATION您的电脑遇到错误需要重启修复
  12. 电影特效合成软件:The Foundry Nuke 11 for Mac
  13. 小米手机怎么在图片显示定位服务器,小米浏览器中,图片导致fixed定位的元素无法显示...
  14. Check Point R80.40 防火墙
  15. 学习并掌握结构化写作方法,提高写作能力 ——结构化写作学习笔记(3)
  16. html css使用特殊自定义字体避免侵权
  17. 在电脑中怎样画思维导图
  18. Linux开放MySql 3306端口
  19. [python]微信公众号JS逆向
  20. 短视频的海绵宝宝配音怎么制作?这可能是最容易上手的配音教程

热门文章

  1. 数字化决策要依靠什么才能实现
  2. js中slice()与splice()的使用与比较,直接看例子记得牢
  3. 软件著作权申请详细流程
  4. IP定位FAQ(“准不准?”)
  5. cocos2d-x游戏中音乐音效的处理
  6. 网站抢购秒杀系统设计简析
  7. 2022-2028全球及中国即时定位与地图构建机器人行业研究及十四五规划分析报告
  8. 友善之臂S70B改成四线触摸
  9. 进程间通信之匿名管道和命名管道
  10. 云南b系列服务器机柜,标准B型网络服务器机柜