侧边栏壁纸
博主头像
Terry

『LESSON 5』

  • 累计撰写 90 篇文章
  • 累计创建 21 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

CAS原理分析

Terry
2020-10-24 / 0 评论 / 0 点赞 / 653 阅读 / 2,844 字 / 正在检测是否收录...

简述

CAS全称是compare and swap,用于多线程环境中实现同步功能的机制,Java中很多锁都是基于CAS实现的。CAS操作包含三个值,内存位置、预期数和新值。CAS的实现逻辑就是将内存位置的数值和预期数值比较,如果相等,则将内存位置的值替换成新值,否则不做任何操作。
Java中没有直接实现CAS(Java不能直接操作内存),CAS通过JNI调用(C++)实现。
CAS的实现其实就是很好运用了处理器支持对指令原子操作,多个核心同时执行一条inc指令会以串行的方式进行。

CAS操作流程

  1. 线程读取数据,这时候线程不加锁
  2. 写回数据,比较原值是否修改,如果未被其他线程修改则写回;如果已被修改,则重新执行读取流程(自旋CAS操作,不是自旋则跳过)

CAS的操作流程其实很简单,就和字面上意思一样,compare and swap,比较和交换。如果发现别的线程未修改则交换,否则继续循环。这是种乐观策略,认为并发操作并不总会发生。

存在问题

其实CAS也存在一些问题,比如说CAS不能保证数据没被别的线程修改过,比如AB问题。

ABA问题

什么是ABA问题

  1. 线程1执行读取操作,获取到了原值A,然后切换线程。
  2. 线程2执行完成CAS操作,将原值A修改为B。
  3. 线程2再次执行CAS操作,将原值N修改为A。
  4. 线程1恢复运行,将比较值和原值进行比较,发现两个值相等。然后用新值写入内存中,完成CAS操作。

所以可以从以上流程看出,线程1不会知道原值其实已经被修改过两次了,只是第2次修改改回了A,看起来没什么变化,线程1因为判断比较值和原值是相等的,所以它会继续往下执行。其实很多场景如果只追求结果的话,修改的过程其实可忽视的。其实对于ABA问题,也是有解决办法,比如在对每一次CAS操作的时候设置版本号。java.util.concurrent.atomic 包下提供了一个可处理 ABA 问题的原子类AtomicStampedReference,大家有兴趣可以看下。

自旋CAS循环时间长导致CPU开销大

自旋CAS原理就是使用了while循环(死循环),如果不成功则一直循环下去,直到成功为止。所以问题来了,如果长时间不成功,会给CPU带来很大的开销。

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

当只对一个共享变量执行操作时,我们可以使用循环CAS的方式保证原子操作AtomicInteger、AtomicBoolean、AtomicXX等等类就是使用循环CAS保证操作原子性。但是如果是对多个共享变量操作,CAS是无法保证原子性操作,我们只能使用锁来操作。不过JDK1.5开始,java.util.concurrent.atomic包下面的AtomicReference类,可以用来保证对象之间的原子性,所以我们可以把多个变量都放在同一个对象里面进行CAS操作,从而保证原子性操作。

扩展

CAS与Synchronized使用场景

  1. 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗CPU资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

synchronized在JDK1.6之后,已经改进优化了。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

JVM中的CAS(堆中对象的分配)

首先,new object()执行的时候,这个对象需要多大的空间,其实是已经确定的,因为java中的各种数据类型,占用多大的空间都是固定的(对其原理不清楚的请自行Google)。那么接下来的工作就是在堆中找出那么一块空间用于存放这个对象。
在单线程的情况下,一般有两种分配策略:

  1. 指针碰撞:这种一般适用于内存是绝对规整的(内存是否规整取决于内存回收策略),分配空间的工作只是将指针像空闲内存一侧移动对象大小的距离即可。
  2. 空闲列表:这种适用于内存非规整的情况,这种情况下JVM会维护一个内存列表,记录哪些内存区域是空闲的,大小是多少。给对象分配空间的时候去空闲列表里查询到合适的区域然后进行分配即可。

但是JVM不可能一直在单线程状态下运行,那样效率太差了。由于再给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两种策略:

  1. CAS:实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。

  2. TLAB:如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。
    虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来进行配置(jdk5及以后的版本默认是启用TLAB的)。

总结

其实CAS原理,就和它的名字一样,compare and swap。看起来实现不是很难,就是运用了处理器支持指令原子操作原理。
关于ABA问题,如果场景只是追求结果,则ABA问题可以忽视,因为结果都是相同的。如果不能忽视,则对每一次CAS操作设置版本号,或者使用java.util.concurrent.atomic 包下提供了一个可处理 ABA 问题的原子类AtomicStampedReference。

参考

CAS原理分析

0

评论区