CAS简介

CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。

CAS 它是一条CPU并发原语。操作包含三个操作数 -- 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作。这个过程是原子的。

CAS并发原语体现在java语言中的sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现汇编指令。这是一种完全依赖硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被打断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

Unsafe类

Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,基于该类可以直接操作特定内存的数据。Unsafe类存在与sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

代码解析

public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5); // 运行结果: true 2019
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t" + atomicInteger.get());
// 运行结果: false 2019
System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t" + atomicInteger.get()); // 此方法可以解决多线程环境下i++问题,底层使用的是Unsafe类CAS和自旋锁
atomicInteger.getAndIncrement();
}
}

源码分析:

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
// this=当前对象 valueOffset=内存偏移量(内存地址) 1=固定值,每次调用+1
// Unsafe就是根据内存偏移地址获取数据的。
return unsafe.getAndAddInt(this, valueOffset, 1);
} /**
* 为了方便查看和添加注释,此方法是从Unsafe类中复制出来的
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 获取var1对象,内存地址在var2的值。
// 相当于这个线程从主物理内存中取值copy到自己的工作内存中。
var5 = this.getIntVolatile(var1, var2); // 比较并交换,如果var1对象,内存地址在var2的值和var5值一样,那么就+1
// compareAndSwapInt如果返回true,取反为false,说明更新成功,退出循环,则返回。
// compareAndSwapInt如果返回false,取反为true,说明当前线程工作内存中的值和主物理内存中的值不一样,被其他线程修改了,则继续循环获取比较,直到更新成功为止。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;
}

执行过程说明:

  • 假设线程A和线程B两个线程同时执行 getAndAddInt操作(分别跑在不同CPU上):
  • AtomicInteger里面的value原始值为3,即主内存中 AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一

    份值为3的value的副本分别到各自的工作内存。
  • 线程A通过 getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。
  • 线程B也通过 getIntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行 compareAndSwapInt方法

    比较内存值也为3,成功修改内存值为4,线程B改完收工,一切OK。
  • 这时线程A恢复,执行 compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已

    经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  • 线程A重新获取 value值,因为变量value被 volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执

    了 compareAndSwapInt进行比较替换,直到成功。

volatile简单说明:

volatile是一个轻量级的同步机制, 三大特性: 保证可见性, 不保证原子性, 禁止指令重排。

  • 可见性: 多个线程从主内存中copy一份数据,修改后,需要将自己的数据重新写入主内存,并通知其他线程数据已更新,保证数据可见性,和多线程数据一致性。
  • 禁止指令重排: 由于指令重排,会对代码的执行顺序进行优化,可能会导致最后的结果和期望的结果不一致,所以需要禁止重排。

CAS的优缺点

优点

  • 不需要加锁,保持了一致性和并发性。

缺点

  • 循环时间长开销很大:我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
  • 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
  • ABA问题:下面会提供详细案例

ABA问题

举个栗子说明:

主内存有个数据值:A,两个线程A和B分别copy主内存数据到自己的工作区,A执行比较慢,需要10秒, B执行比较快,需要2秒, 此时B线程将主内存中的数据更改为B,过了一会又更改为A,然后A线程执行比较,发现结果是A,以为别人没有动过,然后执行更改操作。其实中间已经被更改过了,这就是ABA问题。

也就是ABA问题只要开始时的数据和结束时的数据一致,我就认为没改过,不管过程。

尽管A线程的CAS操作是成功的,但是不代表这个过程就是没问题的。

ABA问题说简单点就是,预判值还是和当初抓取的一样,但是这个“ 值 ”的版本可能不一样了,在某些不仅要考虑数据值是否一致,还要考虑版本是否一致的场景下需要注意.

Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。

解决ABA问题的代码示例

/**
* 解决CAS的ABA问题
*/
public class SolveABAOfCAS { static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); public static void main(String[] args) throws InterruptedException {
System.out.println("==========以下是ABA问题的产生==========");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start(); new Thread(() -> {
try {
// 暂停1秒钟,保证上面完成一次ABA操作
Thread.sleep(1000);
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start(); Thread.sleep(2000);
System.out.println("==========以下是ABA问题的解决==========");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try {
// 暂停一秒钟t3线程
Thread.sleep(1000);
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t3").start(); new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try {
// 暂停3秒钟t4线程,保证上面的t3线程完成一次ABA操作
Thread.sleep(3000);
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改成功否: " + result + "\t当前最新实际版本号: " + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前实际最新值: " + atomicStampedReference.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4").start(); }
}

如果觉得对你有帮助,欢迎来访我的博客:http://jianjieming.com

最新文章

  1. 【CSS】使用CSS选择器(第二部分)
  2. codevs 2606 约数和问题
  3. UI第四节——UIImageView详解
  4. linux在安装jdk时报错
  5. 计算圆周率 Pi (π)值, 精确到小数点后 10000 位 只需要 30 多句代码
  6. 横竖屏事件响应(viewWillLayoutSubviews和通知)两种方式
  7. 本部校赛 蛇形填数(二)problen1338
  8. IIS设置允许下载.exe文件解决方法
  9. 2.1 Java程序的构成
  10. could not get next sequence value
  11. 云计算基础 (redhat7介绍及相关配置)
  12. (十) 编写UVC程序
  13. 学习 Spring (二) Spring 注入
  14. hadoop离线计算项目上线配置问题记录
  15. 2、使用Angular-CLI初始化Angular项目(踩过的深坑!!!)
  16. hdu 2197 求长度为n的本原串 (快速幂+map)
  17. 关于“UI线程”
  18. Kotlin基础学习
  19. 使用 RabbitMQ 实现异步调用
  20. Hadoop HBase概念学习系列之HBase里的Zookeeper(二十一)

热门文章

  1. 新建表需要原表的数据,mysql 如何把查询到的结果插入到新表中
  2. Windows下Go安装&amp;环境配置&amp;编译运行
  3. web自动化环境搭建(python+selenium+webdriver)
  4. rke安装k8s cluster配置
  5. 每天进步一点点------Allegro 手工布线时控制面板各选项说明
  6. 每天进步一点点------Allegro中Autosilk top, Silkscreen top 和Assembly top三个什么区别
  7. 新手学习PHP的避雷针,这些坑在PHP开发中就别跳了
  8. 【转载】Java中的容器讲解
  9. 解决Cannot download &quot;https://github.com/sass/node-sass/releases/download/binding.nod的问题
  10. PHP高并发和大流量怎么解决?