当前位置:首页 » 《资源分享》 » 正文

JavaEE:多线程进阶(CAS)

7 人参与  2024年09月13日 12:02  分类 : 《资源分享》  评论

点击全文阅读


文章目录

CAS什么是 CASCAS 伪代码 CAS有哪些应用CAS的ABA问题什么是ABA问题ABA问题带来的BUG解决方案


CAS

什么是 CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

比较 A 与 V 是否相等。(比较)如果比较相等,将 B 写入V。(交换)返回操作是否成功。

CAS 伪代码

下面写的代码不是原子的, 真实的 CAS 是⼀个原子的硬件指令完成的. 这个伪代码只是辅助理解 CAS的工作流程.

    boolean CAS(address, expectValue, swapValue) {        if (&address == expectedValue){            &address = swapValue;            return true;        }        return false;    }

两种典型的不是"原子性"的代码:

check and set (if 判定然后设定值)[上面的CAS就是这种形式]read and update (i++)

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号.

CAS是乐观锁的一种实现方式

CAS有哪些应用

实现原子类
标准库中提供了java.util.concurrent.atomic包,里面的类都是这种方式来实现的.
在这里插入图片描述

private static AtomicInteger sum = new AtomicInteger();public static void main(String[] args) throws InterruptedException {    Thread t1 = new Thread(()->{        for (int i = 0; i < 50000; i++) {            sum.getAndIncrement(); // sum++            sum.incrementAndGet(); // ++sum            sum.getAndDecrement(); //sum--            sum.decrementAndGet(); // --sum            sum.getAndAdd(10); // sum += 10        }    });    Thread t2 = new Thread(()->{        for (int i = 0; i < 50000; i++) {            sum.getAndIncrement();        }    });    t1.start();    t2.start();    t1.join();    t2.join();    // 通过 sum.get() 来拿到原子类内部持有的真是数据.    System.out.println(sum.get());}

典型的就是AtomicInteger类,其中的getAndIncrement相当于i++操作

AtomicInteger atomicInteger = new AtomicInteger(0);// 相当于 i++atomicInteger.getAndIncrement();

伪代码实现:

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while( CAS( value, oldValue, oldValue+1) != true ) {oldValue = value;}return oldValue;}}

假设两个线程同时调用getAndIncrement
1. 两个线程都读取value的值到oldValue中.(oldValue是一个局部变量,在栈上,每个线程有自己的栈)
在这里插入图片描述
2. 线程1先执行CAS操作.由于oldValue和value的值相同,直接进行对value赋值.

CAS是直接读写内存的,而不是操作寄存器.
CAS的读内存,比较,写内存操作是一条硬件指令,是原子的.

在这里插入图片描述
3. 线程2再执行CAS操作,第一次CAS的时候发现oldValue和value不相等,不能进行赋值,因此需要进入循环.
在循环里重新获取value的值赋给oldValue
在这里插入图片描述
4. 线程2接下来第二次执行CAS,此时oldValue和value相同,于是直接执行赋值操作.
在这里插入图片描述
5. 线程1和线程2返回各自的oldValue的值即可.
通过形如上述代码就可以实现一个原子类.不需要使用重量级锁,就可以高效的完成多线程的自增操作.

本来check and set 这样的操作在代码角度不是原子的,但是在硬件层面上可以让一条指令完成这个操作,也就变成原子的了

实现自旋锁
基于CAS实现更灵活的锁,获取到更多的控制权.
自旋锁伪代码

public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){ this.owner = null; }}

CAS的ABA问题

什么是ABA问题

ABA问题:
假设存在两个线程t1和t2,有一个共享变量num,初始值为A.
接下来,线程t1想使用CAS把num的值改成Z,那么就需要

先读取num的值,记录到oldNum变量中使用CAS判定当前num的值是否为A,如果为A,就修改成Z.
但是,在t1执行这两个操作之间,t2线程可能把num的值从A改成了B,又从B改成了A.

线程t1的CAS是期望num不变就修改.但是num的值已经被t2给修改了,只不过又改成A了,这个时候t1究竟是否要更新num的值为Z呢?

到这一步,t1线程无法区分当前这个变量始终是A,还是经历了一个变化过程.
在这里插入图片描述

ABA问题带来的BUG

大部分的情况下,t2线程这样的一个反复横跳改动,对于t1是否修改num是没有影响的,但是不排除一些特殊情况.

假设我有100存款,我想从ATM取50.取款机创建了两个线程,并发的来执行-50操作.
我们期望一个线程执行-50成功,另一个线程-50失败.
如果使用CAS的方式来完成这个扣款的过程就可能会出现问题.

正常的过程

存款100.线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款值为100,期望更新为50.线程1执行扣款成功,存款被改成50,线程2阻塞等待中.轮到线程2执行了,发现当前存款为50,和之前读到的100不相同,执行失败.

异常的过程

存款100.线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款值为100,期望更新为50.线程1执行扣款成功,存款被改成50,线程2阻塞等待中.在线程2执行之前,我的朋友正好给我转了50,于是我的账户余额就变成了100!轮到线程2执行了,发现当前存款为100,和之前读到的100相同,再次执行扣款操作

这个时候,扣款操作被执行了两次!都怪ABA!!!

解决方案

通过CAS也不是不能解决上述问题.

ABA是因为"余额"能加也能减,才会有ABA问题,如果只能加,不能减就可以解决上述问题.

哎呀,你这不是废话吗,余额怎么可能只加不减?!

别急,余额确实不可能,但是我们可以新建一个变量,让它只增不减.给它起个名字,就叫"版本号"好了.

具体操作如下:
我们可以给要修改的值,引入版本号,在CAS比较数据和当前值和旧值的同时,也要比较版本号是否符合预期.

如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.如果当前版本号高于读到的版本号,那就操作失败(认为数据已经被修改过了)

在Java标准库中提供了AtomicStampedReference<E>类,这个类可以对某个类进行包装,在内部就提供了上面描述的版本管理功能.

本文到这里就结束啦~


点击全文阅读


本文链接:http://zhangshiyu.com/post/159225.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1