大家好,我是树哥。

CAS 操作是高并发场景下,性能如此之高的一个重要优化。今天推荐胜哥的一篇关于 CAS 的文章,带你了解 CAS 的前世今生,写得真是太棒了!

背景

在高并发的业务场景下,线程安全问题是必须考虑的,在JDK5之前,可以通过synchronized或Lock来保证同步,从而达到线程安全的目的。但synchronized或Lock方案属于互斥锁的方案,比较重量级,加锁、释放锁都会引起性能损耗问题。

而在某些场景下,我们是可以通过JUC提供的CAS机制实现无锁的解决方案,或者说是它基于类似于乐观锁的方案,来达到非阻塞同步的方式保证线程安全。

CAS机制不仅是面试中会高频出现的面试题,而且也是高并发实践中必须掌握的知识点。如果你目前对CAS还不甚了解,或许只有模糊的印象,这篇文章一定值得你花时间学习一下。

什么是CAS?

CASCompare And Swap的缩写,直译就是比较并交换。CAS是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令,这个指令会对内存中的共享数据做原子的读写操作。其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新。

本质上来讲CAS是一种无锁的解决方案,也是一种基于乐观锁的操作,可以保证在多线程并发中保障共享资源的原子性操作,相对于synchronized或Lock来说,是一种轻量级的实现方案。

Java中大量使用了CAS机制来实现多线程下数据更新的原子化操作,比如AtomicInteger、CurrentHashMap当中都有CAS的应用。但Java中并没有直接实现CAS,CAS相关的实现是借助C/C++调用CPU指令来实现的,效率很高,但Java代码需通过JNI才能调用。比如,Unsafe类提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

CAS的基本流程

下面我们用一张图来了解一下CAS操作的基本流程。

CAS操作流程图

在上图中涉及到三个值的比较和操作:修改之前获取的(待修改)值A,业务逻辑计算的新值B,以及待修改值对应的内存位置的C。

整个处理流程中,假设内存中存在一个变量i,它在内存中对应的值是A(第一次读取),此时经过业务处理之后,要把它更新成B,那么在更新之前会再读取一下i现在的值C,如果在业务处理的过程中i的值并没有发生变化,也就是A和C相同,才会把i更新(交换)为新值B。如果A和C不相同,那说明在业务计算时,i的值发生了变化,则不更新(交换)成B。最后,CPU会将旧的数值返回。而上述的一系列操作由CPU指令来保证是原子的。

在《Java并发编程实践》中对CAS进行了更加通俗的描述:我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少。

在上述路程中,我们可以很清晰的看到乐观锁的思路,而且这期间并没有使用到锁。因此,相对于synchronized等悲观锁的实现,效率要高非常多。

基于CAS的AtomicInteger使用

关于CAS的实现,最经典最常用的当属AtomicInteger了,我们马上就来看一下AtomicInteger是如何利用CAS实现原子性操作的。为了形成更新鲜明的对比,先来看一下如果不使用CAS机制,想实现线程安全我们通常如何处理。

在没有使用CAS机制时,为了保证线程安全,基于synchronized的实现如下:

public class ThreadSafeTest {public static volatile int i = 0;public synchronized void increase() {i++;}
}

至于上面的实例具体实现,这里不再展开,很多相关的文章专门进行讲解,我们只需要知道为了保证i++的原子操作,在increase方法上使用了重量级的锁synchronized,这会导致该方法的性能低下,所有调用该方法的操作都需要同步等待处理。

那么,如果采用基于CAS实现的AtomicInteger类,上述方法的实现便变得简单且轻量级了:

public class ThreadSafeTest {private final AtomicInteger counter = new AtomicInteger(0);public int increase(){return counter.addAndGet(1);}}

之所以可以如此安全、便捷地来实现安全操作,便是由于AtomicInteger类采用了CAS机制。下面,我们就来了解一下AtomicInteger的功能及源码实现。

CAS的AtomicInteger类

AtomicInteger是java.util.concurrent.atomic 包下的一个原子类,该包下还有AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference等原子类,主要用于在高并发环境下,保证线程安全。

AtomicInteger常用API

AtomicInteger类提供了如下常见的API功能:

public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

上述方法中,getAndXXX格式的方法都实现了原子操作。具体的使用方法参考上面的addAndGet案例即可。

AtomicInteger核心源码

下面看一下AtomicInteger代码中的核心实现代码:

public class AtomicInteger extends Number implements java.io.Serializable {private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {// 用于获取value字段相对当前对象的“起始地址”的偏移量valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;//返回当前值public final int get() {return value;}//递增加detlapublic final int getAndAdd(int delta) {// 1、this:当前的实例 // 2、valueOffset:value实例变量的偏移量 // 3、delta:当前value要加上的数(value+delta)。return unsafe.getAndAddInt(this, valueOffset, delta);}//递增加1public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}
...
}

上述代码以AtomicInteger#incrementAndGet方法为例展示了AtomicInteger的基本实现。其中,在static静态代码块中,基于Unsafe类获取value字段相对当前对象的“起始地址”的偏移量,用于后续Unsafe类的处理。

在处理自增的原子操作时,使用的是Unsafe类中的getAndAddInt方法,CAS的实现便是由Unsafe类的该方法提供,从而保证自增操作的原子性。

同时,在AtomicInteger类中,可以看到value值通过volatile进行修饰,保证了该属性值的线程可见性。在多并发的情况下,一个线程的修改,可以保证到其他线程立马看到修改后的值。

通过源码可以看出, AtomicInteger 底层是通过volatile变量和CAS两者相结合来保证更新数据的原子性。其中关于Unsafe类对CAS的实现,我们下面详细介绍。

CAS的工作原理

CAS的实现原理简单来说就是由Unsafe类和其中的自旋锁来完成的,下面针对源代码来看一下这两块的内容。

UnSafe类

在AtomicInteger核心源码中,已经看到CAS的实现是通过Unsafe类来完成的,先来了解一下Unsafe类的作用。关于Unsafe类在之前的文章《各大框架都在使用的Unsafe类,到底有多神奇?》也有详细的介绍,大家可以参考,这里我们再简单概述一下。

sun.misc.Unsafe是JDK内部用的工具类。它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。该类不应该在JDK核心类库之外使用,这也是命名为Unsafe(不安全)的原因。

JVM的实现可以自由选择如何实现Java对象的“布局”,也就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。

Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java对象的某个字段。在AtomicInteger的static代码块中便使用了objectFieldOffset()方法。

Unsafe类的功能主要分为内存操作、CAS、Class相关、对象操作、数组相关、内存屏障、系统相关、线程调度等功能。这里我们只需要知道其功能即可,方便理解CAS的实现,注意不建议在日常开发中使用。

Unsafe与CAS

AtomicInteger调用了Unsafe#getAndAddInt方法:

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

上述代码等于是AtomicInteger调用UnSafe类的CAS方法,JVM帮我们实现出汇编指令,从而实现原子操作。

在Unsafe中getAndAddInt方法实现如下:

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

getAndAddInt方法有三个参数:

  • 第一个参数表示当前对象,也就是new的那个AtomicInteger对象;

  • 第二个表示内存地址;

  • 第三个表示自增步伐,在AtomicInteger#incrementAndGet中默认的自增步伐是1。

getAndAddInt方法中,首先把当前对象主内存中的值赋给val5,然后进入while循环。判断当前对象此刻主内存中的值是否等于val5,如果是,就自增(交换值),否则继续循环,重新获取val5的值。

在上述逻辑中核心方法是compareAndSwapInt方法,它是一个native方法,这个方法汇编之后是CPU原语指令,原语指令是连续执行不会被打断的,所以可以保证原子性。

在getAndAddInt方法中还涉及到一个实现自旋锁。所谓的自旋,其实就是上面getAndAddInt方法中的do while循环操作。当预期值和主内存中的值不等时,就重新获取主内存中的值,这就是自旋。

这里我们可以看到CAS实现的一个缺点:内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。如果长时间都不成功的话,就会造成CPU极大的开销。

另外,Unsafe类还支持了其他的CAS方法,比如compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong。更多关于Unsafe类的功能就不再展开,大家可以去看《各大框架都在使用的Unsafe类,到底有多神奇?》这篇文章。

CAS的缺点

CAS高效地实现了原子性操作,但在以下三方面还存在着一些缺点:

  • 循环时间长,开销大;

  • 只能保证一个共享变量的原子操作;

  • ABA问题;

下面就这个三个问题详细讨论一下。

循环时间长开销大

在分析Unsafe源代码的时候我们已经提到,在Unsafe的实现中使用了自旋锁的机制。在该环节如果CAS操作失败,就需要循环进行CAS操作(do while循环同时将期望值更新为最新的),如果长时间都不成功的话,那么会造成CPU极大的开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升。

只能保证一个共享变量的原子操作

在最初的实例中,可以看出是针对一个共享变量使用了CAS机制,可以保证原子性操作。但如果存在多个共享变量,或一整个代码块的逻辑需要保证线程安全,CAS就无法保证原子性操作了,此时就需要考虑采用加锁方式(悲观锁)保证原子性,或者有一个取巧的办法,把多个共享变量合并成一个共享变量进行CAS操作。

ABA问题

虽然使用CAS可以实现非阻塞式的原子性操作,但是会产生ABA问题,ABA问题出现的基本流程:

  • 进程P1在共享变量中读到值为A;

  • P1被抢占了,进程P2执行;

  • P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占;

  • P1回来看到共享变量里的值没有被改变,于是继续执行;

虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)。

维基百科上给了一个形象的例子:你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

ABA问题的解决思路就是使用版本号:在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。

另外,从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

小结

本文从CAS的基本使用场景、基本流程、实现类AtomicInteger源码解析、CAS的Unsafe实现解析、CAS的缺点及解决方案等方面来全面了解了CAS。通过这篇文章的学习想必你已经更加深刻的理解CAS机制了,如果对你有所帮助,记得关注一下,持续输出干货内容。

看完文章觉得没弄懂

想和更多技术小伙伴交流?

扫码添加树哥微信,回复「入群」即可加入社群!


推荐阅读

  • 一次线上事故,我顿悟了异步的精髓!

  • Java 中为什么不全部使用 static 方法?

  • 一文带你弄懂 CDN 的技术原理!

  • 如何设计一个分布式 ID 发号器?

  • 关于 CMS 垃圾回收器,你真的懂了吗?

  • 接口流量突增,如何做好性能优化?

  • MySQL 啥时候用表锁,啥时候用行锁?

  • 从全局角度,如何设计一个秒杀系统?

  • 系统总出故障怎么办,或许你该学学稳定性建设!

  • 服务器宕机了,Kafka 消息会丢失吗?

高并发必学的 CAS 操作,看这篇就够了!相关推荐

  1. 99. 中高级开发面试必问的Redis,看这篇就够了

    中高级开发面试必问的Redis,看这篇就够了! 一.概述 二.数据类型 STRING LIST SET HASH ZSET 三.数据结构 字典 跳跃表 四.使用场景 计数器 缓存 查找表 消息队列 会 ...

  2. 整理了一份 Docker系统知识,从安装到熟练操作看这篇就够了 | 原力计划

    作者 | IronmanJay 责编 | 王晓曼 出品 | CSDN博客 Docker 基础介绍 下图为 Docker 图标(是一个鲸鱼上面是集装箱). 1.我们为什么要使用Docker 当我们在工作 ...

  3. docker停止容器后配置_整理了一份 Docker系统知识,从安装到熟练操作看这篇就够了 | 原力计划...

    作者 | IronmanJay 责编 | 王晓曼 出品 | CSDN博客 Docker 基础介绍 下图为 Docker 图标(是一个鲸鱼上面是集装箱). 1.我们为什么要使用Docker 当我们在工作 ...

  4. 高版本Andriod Studio集成HMS环境看这篇就够了(附加步骤多图、资源下载、源代码、问题总结)

    Aandriod Studio集成HMS环境 0.前言 1.开发环境介绍 a) Java版本 b) Android Studio版本 c) Gradle/SDK版本 2.注册认证华为开发者联盟(个人开 ...

  5. python中tkinter模块窗口操作_Python GUI之tkinter窗口视窗教程大集合(看这篇就够了)...

    本篇博文搬到个人博客:[洪卫の博客](https://sunhwee.com)上面去了,想要获得最佳阅读体验,欢迎前往 [https://sunhwee.com](洪卫の博客), 建议用电脑查看教程文 ...

  6. Docker入门实战看这篇就够了(最新详细以及踩过的坑)

    Docker入门实战看这篇就够了 前言 初识 是什么 容器与虚拟机 能干什么 去哪玩 安装 先决条件 查看自己的内核 安装所需的软件包(支持devicemapper存储类型) 设置镜像的仓库 设置yu ...

  7. 【超全汇总】学习数据结构与算法,计算机基础知识,看这篇就够了【ZT帅地】2020-3-7

    https://blog.csdn.net/m0_37907797/article/details/104029002 由于文章有点多,并且发的文章也不是一个系列一个系列发的,不过我的文章大部分都是围 ...

  8. React入门看这篇就够了

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: 很多值得了解的细节. 原文:React入门看这篇就够了 作者:Random Fundebug经授权转载,版权归原作者所 ...

  9. Spring Cloud入门,看这篇就够了!

    点击▲关注 "中生代技术"   给公众号标星置顶 更多精彩 第一时间直达 概述 首先我给大家看一张图,如果大家对这张图有些地方不太理解的话,我希望你们看完我这篇文章会恍然大悟. 什 ...

最新文章

  1. aide java 线程类_aide_Java常用关键字、方法使用实例
  2. Spring JDBC-实施Spring AOP事务注意事项及案例分析
  3. 全民学英语,VIPKID、51Talk、伴鱼、阿卡索们还好做么?
  4. Spring的bean实例化过程
  5. Python学习之路20-数据模型
  6. 开工大吉:Oracle 18c已经发布及新特性介绍
  7. python画动态玫瑰花图片大全_【python】trutle绘制送给女神的玫瑰花图
  8. 拓端tecdat|使用Python中的ImageAI进行对象检测
  9. 学生成绩管理系统(C语言实现)
  10. 结构建模设计——Solidworks 2021SP5稳定版软件安装详细教程(安装踩坑记录全流程总结)
  11. Excel使用VBA小程序的方法
  12. java 数字信号_GitHub - Bazingaliu/JavaDsp: 数字信号处理(DSP)方面的Java封装,包含常用的一些处理方法,如滤波、信号变换等等。...
  13. WAREZ无形帝国(盗版之源)
  14. Python游戏开发实战:飞机大战(含代码)
  15. Pascal 转 C++ 教程1
  16. SAP中多层扩展有效地bom
  17. 小猿圈python金角大王_小猿圈python学习-基本数据类型
  18. 导致MySQL的查询语句效率低下的可能原因
  19. 首涂[A020]第二十套模板最终版 苹果CMS V10
  20. 计算机应用行距怎么弄,电脑行间距在哪里设置

热门文章

  1. 凯斯轴承数据故障诊断PHM轴承寿命预测深度学习迁移学习元学习开源代码集合
  2. Matlab——运输问题
  3. c语言链表中何时用点何时用箭头,链表基本操作及其过程详细叙述
  4. 清理windows的弹窗的办法
  5. php美化字体带边框,还可以为其设置带图案的线条做为字体边框
  6. 2023系统分析师---论高可靠性系统中软件容错性技术的应用(付费资料)
  7. 卡丹尔算法(max subarray problem)
  8. 微信服务号模板消息推送
  9. iframe交互问题,浏览器存在差异
  10. 了解浏览器cookie以及浏览器缓存