Java多线程(十一)

目录

一、什么是CAS

二、CAS 是怎么实现的

三、CAS的应用

3.1 实现原子类

3.2 实现自旋锁

四、CAS的ABA问题

4.1 什么是ABA问题

4.2 ABA问题引发的BUG

4.3 ABA问题的解决方案

五、CAS与加锁的区别

扫描二维码关注公众号,回复: 16749872 查看本文章

一、什么是CAS

CAS:全称Compare and swap,也就是“比较并交换”,一个CAS涉及到一下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1. 比较 A 与 V 是否相等。(这里就是判断该值是否被修改过)
2. 如果比较相等,将 B 写入 V。(交换)
3. 返回操作是否成功。

CAS伪代码(无法直接编译运行)

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

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

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

二、CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。

简而言之,是因为硬件予以了支持,软件层面才能做到

这里的实现就不具体展开了,主要是由硬件提供的支持

三、CAS的应用

3.1 实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.典型的就是 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)这里发现线程2先执行CAS操 value 与 oldValue 的值一致,这里并没有被修改过,所以直接将 oldValue+1value 进行赋值。结果返回 true 不进入循环最后线程2返回 oldValue 的0(因为这里是后置++,所以返回的是计算前的值)

注意:

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

3)线程1继续执行CAS操作,第一次CAS的时候发现 value oldValue 不相等,CAS直接返回false进入循环,然后从主内存中读取最新的value值。

4)因为是循环操作,接下来将进行第二次的CAS,这里发现 value oldValue 相等了,于是直接进行赋值操作。

5)线程1和线程2返回各自的 oldValue 值即可

通过形如上述代码就可以实现一个原子类,不需要使用重量级锁,就可以高效的完成多线程的自增操作

3.2 实现自旋锁

利用CAS来实现更加灵活的锁,以便获得更多的控制权。这里用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问题

4.1 什么是ABA问题

通俗的说就是某个东西一开始是A,但是经过修改变成了B,但是最后又修改成了A,这就是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问题

4.2 ABA问题引发的BUG

大多数情况下ABA问题是不会影响到什么的,但是不排除会出现特殊的情况

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

正常的过程

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

异常的过程

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

这种特殊的情况下由于ABA问题就会痛失50!!

4.3 ABA问题的解决方案

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

  • CAS 操作在读取旧值的同时, 也要读取版本号.

真正修改的时候

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

这就好比, 判定这个手机是否是翻新机, 那么就需要收集每个手机的数据, 第一次挂在电商网站上的手机记为版本1(刚刚出厂), 以后每次这个手机出现在电商网站上, 就把版本号进行递增(变成二手、三手). 这样如果买家不在意这是翻新机, 就买. 如果买家在意, 就可以直接略过

利用版本号再次解决上述转账案例:

为了解决 ABA 问题, 给余额搭配一个版本号, 初始设为 1.

  1. 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100,版本号为 1, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读到的版本号为 1, 版本小于当前版本, 认为操作失败.

五、CAS与加锁的区别

加锁保证线程安全:多线程加锁是强制避免出现穿插的情况。

CAS保证线程安全:借助CAS来识别是否出现了穿插的情况,如果没有出现穿插情况说明是安全的,就直接修改。如果出现了穿插的情况就说明线程不安全,就需要重新读取内存中的值,然后再尝试修改。

猜你喜欢

转载自blog.csdn.net/x2656271356/article/details/132461050
今日推荐