【多线程 六】彻彻底底搞懂CAS,解决ABA问题 您所在的位置:网站首页 怎么彻底解决台湾问题 【多线程 六】彻彻底底搞懂CAS,解决ABA问题

【多线程 六】彻彻底底搞懂CAS,解决ABA问题

2024-07-10 17:33| 来源: 网络整理| 查看: 265

前言:

如果不知道JMM(java 内存模型),还请看博主的上一篇博客: volatile关键字与内存可见性详解 ,因为本博客还需要JMM的理论基础。

博客内容导图

在这里插入图片描述

1、CAS是什么

CAS(Compare-And-Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问. CAS 是一种无锁的非阻塞算法的实现。 以上是一本书给出的定义。其实我们在上篇博客已经提过CAS了,AtomicInteger 实现num++,AtomicInteger的底层就是用了CAS的思想。CAS是一条CPU并发原语.并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致(线程安全)的问题.

2、CAS解决了什么问题

对于多线程而言,实现线程安全是最要的也是最基本的,我们之前通过同步锁synchronized()实现,它保证了线程安全,也就是数据的一致性,但是它的并发性下降,因为每次在都要对代码块进行加锁,线程得到锁才可以运行。但是CAS是一种无锁的非阻塞的算法实现,如果线程数不多(并发量小),他的性能要比synchronized()高的多,但是线程数过多,就会过分的消耗cpu资源(具体为啥后面会解释)。

3、CAS的原理

CAS 包含了 3 个操作数:

需要读写的内存值 V 进行比较的值 A 拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作。接下来用源码来验证以上理论。

3.1 atomic包中的类

且看AtomicInteger,AtomicInteger位于java.util.concurrent.atomic包下,如下图:(本图截取自JDK API 1.6中文版) 在这里插入图片描述 从上图可知道,atomic包下都是原子性的操作,原子性就是说无论是几行代码,要么这些代码一块执行,要么就不执行,中间不可以 加塞(比如还没执行完,发生线程调度),这就完美的解决num++线程不安全的问题。

3.2 根据atomicInteger.getAndIncrement()源码验证CAS理论

(1)从下面的代码可以看出,用到了Unsafe这个类,UnSafe类在sun.misc包中,UnSafe是CAS的核心类 ,由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问, 基于该类可以直接操作特额定的内存数据,其内部方法操作可以像C的指针一样直接操作内存。 UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务

看到这里的value是用关键字volatile修饰的,保证了内存可见性

public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;

(2)如下面的代码: unsafe.getAndAddInt(this, valueOffset, 1),传了三个参数,分别是当前对象this、该对象在内存中的偏移地址,还有表示每次都加1的数字1。其中UnSafe就是根据内存偏移地址获取数据的偏移地址

/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }

(3)如下代码,进入到方法getAndAddInt,发现是一个do while 循环,接下来进行说明:

var1 AtomicInteger对象本身 var2 该对象值的引用地址 var4 需要变动的数量 var5 是通过var1 var2找出内存中真实的值

this.compareAndSwapInt(var1, var2, var5, var5 + var4),表示的是该对象var1在内存中的偏移量var2位置的值与期望值var5进行比较,如果相同,更新var5+var4的值并且返回true,如果不同,继续循环,将期望值var5改为内存中真正的值,然后在进行比较,直到更新完成。

综上也体现出了它的缺点,就是如果线程过多,其他的线程可能会一直改内存中的实际值,那么期望值var5就会很难和内存中的实际值相等,这样就会导致一直循环,效率下降,所以如果线程多,并发大,用锁比较合适。

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

(4)CAS原理分析 1.假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上)且AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.

2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这时线程A被挂起.线程B也通过getIntVolatile(var1,var2) 拿到value值3, 在这里插入图片描述

3.此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 ,成功修改内存的值为4 线程B走完收工 一切OK。 在这里插入图片描述

4.这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数值4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.

在这里插入图片描述 5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.

4、CAS的缺点

(1)根据上面所说的原理,如果并发量特别大或者说开启的线程数过多,就可能会存在内存中的共享的值经常被修改,导致unsafe底层匹配不成功,会一直do-while下去,加大了cpu 的压力 (2)只能保证一个共享变量的原子操作。(比如声明两个原子变量a和b 具体操作是a+b,此时只能枷锁了)

(3)引发ABA问题(狸猫换太子) ABA问题举例: 场景1: 一个线程1从内存中取出A,这个时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,线程1此时还被阻塞,线程2又进行了一些操作,然后将B又变成了A,此时线程1获得资源,开始执行,但是在进行cas操作的时候发现内存中还是A,然后线程1执行成功。(说白了就是可能存在一个线程根本不知道数值发生了变化)

场景2:

ABA问题:内存值V=100; threadA 将100,改为50; threadB 将100,改为50; threadC 将50,改为100;

小牛取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB工作,此时threadA操作成功(100->50),threadB,取完值100,然后阻塞。正好牛妈打款50元给小牛(50->100),threadC执行成功,之后threadB运行了,又改为(100->50)。 牛气冲天,lz钱哪去了???

5、如何解决ABA问题:

对内存中的值加个版本号,在比较的时候除了比较值还的比较版本号。 AtomicStampedReference就是用版本号实现cas机制,再讲AtomicStampedReference之前,先讲解AtomicReference原子引用。

5.1 AtomicReference原子引用

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。我们可以自定义对象。

代码如下:

public class TestAtomic { public static void main(String[] args) { AtomicReference atomicUser=new AtomicReference(); User zhangsan=new User("张三",21); User lisi =new User("李四",25); atomicUser.set(zhangsan); System.out.println(atomicUser.compareAndSet(zhangsan,lisi)+"\t"+atomicUser.get().toString()); } } class User{ private String username; private int age; public User(String username,int age){ this.username=username; this.age=age; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", age=" + age + '}'; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }

执行结果: 在这里插入图片描述 // 分析: 定义了对象User,利用方法compareAndSet进行比较和判断,内存中存入了张三,然后和compareAndset的第一个参数进行比较,发现都是张三,然后修改为lisi。

5.2 版本号原子引用 AtomicStampedReference解决ABA 问题

AtomicStampedReference 增加了版本号,解决了ABA问题。 思想: 内存值V=100; threadA 获取值100 版本号1,改为50 版本号2; threadB 获取值100 版本号1 threadC 将50 版本号2 改为100 版本号3

场景:小牛取款,由于机器不太好使,多点了几次取款操作。后台threadA和threadB工作,此时threadA操作成功(100->50) 版本号1->版本号2,threadB取完值100,版本号是1,然后阻塞。正好牛妈打款50元给小牛(50->100) 版本号2->版本号3,threadC执行成功,之后threadB获取资源,开始执行,发现内存的值为100, 但是版本号不对应, 所以重新从内存中读取值,然后再进行判断。这次牛就不会冲天了,哈哈哈哈哈!

以上场景对应代码:

public class ABADemo { // 表示内存里初始值是100,版本好号为1 private static AtomicStampedReference atomicStampedReference=new AtomicStampedReference(100,1); public static void main(String[] args) { new Thread(()->{ // 获得版本号 int stamp = atomicStampedReference.getStamp(); // 这里延迟一秒不是为了说为了线程阻塞,而是为了让线程B得到版本号 System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 期望值为100 和内存中的值进行比较,如果一样,且版本号stamp也和内存中一样,则改为50 atomicStampedReference.compareAndSet(100,50,stamp,stamp+1); System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+atomicStampedReference.getStamp()); // atomicStampedReference.compareAndSet(100,50,stamp,stamp+1); },"A").start(); new Thread(()->{ // 获得版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp); // 线程阻塞 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } // 期望值为100 和内存中的值进行比较,如果一样,且版本号stamp也和内存中一样,则改为50 boolean result = atomicStampedReference.compareAndSet(100, 50, stamp, stamp + 1); System.out.println(Thread.currentThread().getName()+"\t修改是否成功"+result+"\t当前实际版本号:"+atomicStampedReference.getStamp()); System.out.println("当前实际最新值:"+atomicStampedReference.getReference()); },"B").start(); new Thread(()->{ // 这里延迟两秒不是为了说为了线程阻塞,而是为了让线程A 执行完毕 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 获得版本号 int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t第一次版本号:"+stamp); // 期望值为100 和内存中的值进行比较,如果一样,且版本号stamp也和内存中一样,则改为50 atomicStampedReference.compareAndSet(50,100,stamp,stamp+1); System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+atomicStampedReference.getStamp()); },"C").start(); } }

运行结果: 在这里插入图片描述

后记:

总结不易,如果对你有帮助,请点个赞欧!

·



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有