CAS原理机制分析

浅浅的花香味﹌ 2022-12-11 04:26 295阅读 0赞

一、CAS

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V旧的预期值A将要更新的目标值B

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作

二、案例分析

1.普通多线程count自增运算CASTest1.java

  1. package com.cas;
  2. public class CASTest1 {
  3. public static int count = 0;
  4. // 创建5个线程,在每个线程当中让count自增100次
  5. // 由于线程不安全,所以这段代码最终的运行结果中count很有可能会小于500
  6. public static void main(String[] args) {
  7. for (int i = 0; i < 5; i++) {
  8. new Thread(new Runnable() {
  9. public void run() {
  10. try {
  11. Thread.sleep(10);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. for (int j = 0; j < 100; j++) {
  16. count++;
  17. }
  18. }
  19. }).start();
  20. }
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. System.out.println("count=" + count);
  27. }
  28. }

CASTest1,多次计算结果,有可能值等于500,也有可能值小于500,异常结果如下:
在这里插入图片描述

2.使用Synchronized同步锁,报障线程安全,count自增运算CASTest2.java

  1. package com.cas;
  2. public class CASTest2 {
  3. public static int count = 0;
  4. // 创建5个线程,在每个线程当中让count自增100次
  5. // 使用Synchronized同步锁,报障线程安全,count最终结果等于500
  6. public static void main(String[] args) {
  7. for (int i = 0; i < 5; i++) {
  8. new Thread(new Runnable() {
  9. public void run() {
  10. try {
  11. Thread.sleep(10);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. for (int j = 0; j < 100; j++) {
  16. synchronized (CASTest2.class) {
  17. count++;
  18. }
  19. }
  20. }
  21. }).start();
  22. }
  23. try {
  24. Thread.sleep(1000);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. System.out.println("count=" + count);
  29. }
  30. }

CASTest2.java计算结果如下
在这里插入图片描述

3.使用Atomic操作类,提升性能,CASTest3.java

  1. package com.cas;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3. public class CASTest3 {
  4. public static AtomicInteger count = new AtomicInteger();
  5. // 创建5个线程,在每个线程当中让count自增100次
  6. // 使用AtomicInteger,Atomic操作类的底层正是用到了“CAS机制”,保障原子性操作,count最终结果等于500
  7. public static void main(String[] args) {
  8. for (int i = 0; i < 5; i++) {
  9. new Thread(new Runnable() {
  10. public void run() {
  11. try {
  12. Thread.sleep(10);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. // 每个线程当中让count值自增100次
  17. for (int j = 0; j < 100; j++) {
  18. //使用incrementAndGet方法进行自增运算
  19. count.incrementAndGet();
  20. }
  21. }
  22. }).start();
  23. }
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. System.out.println("count=" + count);
  30. }
  31. }

CASTest3.java计算结果如下
在这里插入图片描述

三、CAS底层原理分析

1.CAS和Synchronized对比

Synchronized关键字会让没有得到锁资源的线程进入Blocked状态,而后在争夺到锁资源后恢复为Runing状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高
另外,从思想上来说,Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守。而CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新

尽管JAVA 1.6之后为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低(Synchronized转变为重量级锁之前,也会采用CAS机制)。所以面对这种情况,我们就可以使用Java中的原子操作类

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。而Atomic操作类的底层正是用到了“CAS机制”。

2.CAS的缺点

(1)CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
(2)不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
(3) ABA问题及解决方案
CAS 的使用流程通常如下:
1)首先从内存地址 V 读取值 A;
2)根据 A 计算目标值 B;
3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?

比如链表处理,出现ABA时可能会掉链,因为虽然链表头指针相同,但是链表的后续节点值有可能不同。
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。

Java并发包为了解决ABA问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效

原子引用解决ABA问题代码案例

  1. package com.zhang;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicReference;
  4. import java.util.concurrent.atomic.AtomicStampedReference;
  5. public class ABADemoTest {
  6. //原子引用类,解决ABA问题
  7. static AtomicReference atomicReference = new AtomicReference(100);//初始值为100
  8. static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);//初始值为100,版本号为1
  9. public static void main(String[] args) {
  10. System.out.println("以下是ABA问题的产生==========");
  11. new Thread(() ->{
  12. atomicReference.compareAndSet(100,101);
  13. atomicReference.compareAndSet(101,100);
  14. },"T1").start();
  15. new Thread(() -> {
  16. //暂停1秒,保证t1先执行,t2后执行
  17. try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
  18. System.out.println("这里产生了ABA问题,发现值被改成了2021,CAS结果为:"+atomicReference.compareAndSet(100, 2021)+"\t"+atomicReference.get());
  19. },"T2").start();
  20. //暂停2秒
  21. try { TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) { e.printStackTrace();}
  22. System.out.println("以下是ABA问题的解决==========");
  23. new Thread(()->{
  24. int stamp = atomicStampedReference.getStamp();//获取初始版本号
  25. System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
  26. try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
  27. //4个参数分别为期望值、更新值、当前版本号、更新后版本号
  28. atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);//执行CAS操作并更新版本号
  29. System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
  30. atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);//执行CAS操作并更新版本号
  31. System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
  32. },"T3").start();
  33. new Thread(()->{
  34. int stamp = atomicStampedReference.getStamp();//获取初始版本号
  35. System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
  36. try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
  37. boolean result = atomicStampedReference.compareAndSet(100,2021,stamp,stamp+1);//乐观认为数据100没有被改变,尝试执行将数据100更新成2021操作
  38. System.out.println(Thread.currentThread().getName()+"\t修改成功否:"+result+"\t当前最新版本号:"+atomicStampedReference.getStamp());
  39. System.out.println(Thread.currentThread().getName()+"\t当前实际最新值:"+atomicStampedReference.getReference());
  40. },"T4").start();
  41. }
  42. }

执行结果如下:
在这里插入图片描述

3.CAS的底层实现

(1)针对案例3中CASTest3.java的incrementAndGet()方法
通过方法调用,我们可以发现,getAndIncrement()方法调用getAndAddInt()方法,最后调用的是compareAndSwapInt()方法。如下两张图片来自网上博客getAndIncrement()方法。
在这里插入图片描述
在这里插入图片描述
getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试将内存位置的值修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试(CAS的自旋操作),直至修改成功。
用volatile关键字来保证(保证线程间的可见性)获取的当前值是内存中的最新值。

(2)什么是unsafe呢?
Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

参考资料
https://blog.csdn.net/v123411739/article/details/79561458
https://blog.csdn.net/Metis\_/article/details/79380378
https://blog.csdn.net/qq\_32998153/article/details/79529704

发表评论

表情:
评论列表 (有 0 条评论,295人围观)

还没有评论,来说两句吧...

相关阅读

    相关 CAS原理分析

    CAS的英文为Compare and Swap 翻译为比较并交换。 CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种

    相关 CAS原理分析

    1 概述 CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问

    相关 JAVA CAS原理深度分析

    看了一堆文章,终于把JAVA CAS的原理深入分析清楚了。 感谢GOOGLE强大的搜索,借此挖苦下百度,依靠百度什么都学习不到!   参考文档: http://www.

    相关 Java CAS 原理分析

    1.简介 CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制(可以把 CAS 看做乐观锁)。CAS 操作包含三个操作数 -- 内