深度理解CAS和ABA问题
一》对于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问题相关推荐
- 无锁数据结构--理解CAS、ABA、环形数组
在分布式系统中经常会使用到共享内存,然后多个进程并行读写同一块共享内存,这样就会造成并发冲突的问题, 一般的常规做法是加锁,但是锁对性能的影响非常大. 无锁队列是一个非常经典的并行计算数据结构,它极大 ...
- 高并发之深度解析CAS [理论+案例+源码]
目录 1.理论 2.问题 3.实操--Atomic打头的类 4.原理:基于类Striped64分散热点[空间换时间] 1.理论 CAS,即compare and swap,比较并交换,包含三个操作数, ...
- atomic原子类实现机制_并发编程:并发操作原子类Atomic以及CAS的ABA问题
本文基于JDK1.8 Atomic原子类 原子类是具有原子操作特征的类. 原子类存在于java.util.concurrent.atmic包下. 根据操作的数据类型,原子类可以分为以下几类. 基本类型 ...
- java购买同一件商品时加锁_java中CAS的ABA问题思考和整理(不看后悔系列)
前言 听说经常面试被问到~今天同事说了这个问题,就查了一下这问题,觉得挺有意思的,就整理出来跟大家分享下.主要思考下面几个问题: 1.什么是CAS? 2.什么是CAS的ABA问题? 3.怎么解决这个问 ...
- 沈向洋:从深度学习到深度理解
2020-07-30 01:29:20 作者 | 蒋宝尚 编辑 | 丛 末 7月19日,深圳市人工智能与机器人研究院与香港中文大学(深圳)联合主办的"全球人工智能与机器人前沿研讨会" ...
- 深度理解do{} while(0)语句的作用
深度理解do{} while(0) 在linux内核中常常会看到do{} while(0)这样的语句,有人疑惑,认为无意义,因为他只执行一次,加不加do{} while(0)小过失完全一样的,那你就错 ...
- CAS的ABA问题描述 AtomicStampReference
CAS的ABA问题描述 在CAS操作的时候,其他线程将当前变量的值从A改成B,又改回A: CAS线程用期望值A与当前变量比较的时候,发现当前变量没有变,于是CAS就将当前变量进行了交换操作,但其实当前 ...
- go之树型结构深度理解补充
go之树型结构深度理解补充 在上一篇中借用了 Ilija Eftimov 文章来讲解了tree的定义和一些方法.这篇文章主要是讲解在树型结构中如何判断节点与节点之间的关系. A节点是否是B节点的直接上 ...
- 【TensorFlow】TensorFlow从浅入深系列之二 -- 教你通过思维导图深度理解深层神经网络
本文是<TensorFlow从浅入深>系列之第2篇 TensorFlow从浅入深系列之一 -- 教你如何设置学习率(指数衰减法) TensorFlow从浅入深系列之二 -- 教你通过思维导 ...
最新文章
- java c 序列化_Java 序列化
- 计算机动画制作 实验要求,有关计算机动画设计课程教学的对比试验
- dataframe保存为txt_如何批量查找并修改替换 Word、PPT、Excel、PDF、TXT等文件的内容...
- windows下使用pthread库
- css提取页面元素唯一性_下面这个函数,能够获取一个元素的任意 CSS 属性值。...
- 如何避免mysql回表查询_mysql如何避免回表查询
- Jupyter notebook中怎么添加Pytorch运行环境
- 严肃贴:内幕 手机行业
- android 日历_适用于Android的十大最佳日历应用
- react 点击使父元素消失_在 React 组件中使用 Refs 指南
- win8蓝屏错误代码DPC_WATCHDOG_VIOLATION您的电脑遇到错误需要重启修复
- 电影特效合成软件:The Foundry Nuke 11 for Mac
- 小米手机怎么在图片显示定位服务器,小米浏览器中,图片导致fixed定位的元素无法显示...
- Check Point R80.40 防火墙
- 学习并掌握结构化写作方法,提高写作能力 ——结构化写作学习笔记(3)
- html css使用特殊自定义字体避免侵权
- 在电脑中怎样画思维导图
- Linux开放MySql 3306端口
- [python]微信公众号JS逆向
- 短视频的海绵宝宝配音怎么制作?这可能是最容易上手的配音教程