美文网首页Android技术知识Android进阶之路
Android并发内存——CAS(CAS原理+原子操作+原子性问

Android并发内存——CAS(CAS原理+原子操作+原子性问

作者: 码农的地中海 | 来源:发表于2022-07-17 21:53 被阅读0次

    前言

    Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值;一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

    JAVA中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS.

    CAS优缺点

    优点:

    由于CAS是非阻塞的,可避免死锁,线程间的互相影响非常小。
    没有锁竞争带来的系统开销,也没有线程间频繁调度的开销。

    缺点(重点):

    自旋循环时间长开销大
    如果某个线程通过CAS方式操作某个变量不成功,长时间自旋,则会对CPU带来较大开销。
    解决方式:限制自旋次数

    ABA问题

    假如线程T1取得在内存中offset处的值是A,那在赋值的过程中取到内存中的值还是A,此时我们能确定内存中的值A并没有被其他线程修改过吗?当然不行,我给你举一个例子

    线程T1和T2一开始取得内存中的旧值都是A,但是线程T2运行的快,将A->B->A,所以T1进行CAS的时候依旧可以通过,误认为内存中的值重来没有被修改过,这实际上是不对的,这就是“ABA”问题。
      解决方式:Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedRefernce来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查标志stamped是否为预期标志,如果全部一致,则继续。

    原子性概念

    原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其它的线程干扰。

    例如语句(a++)实际上包含了三个操作:

    1. 读取变量a的值
    2. 对a进行加1的操作
    3. 将计算后的值再赋值给变量a
      像这三个操作就无法构成原子性操作。

    原子类的工作原理-CAS机制

    2.1 原子类概述

    在java.util.concurrent.atomic包下定义了一些对“变量操作”的“原子类”,例:

    java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
    java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
    java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
    像这些原子类都可以保证对变量操作的:原子性、有序性、可见性。

    2.2 AtomicInteger类示例

    我们可以通过AtomicInteger类,来看看原子类是如何使用的:

    1.线程类:

    public class MyThread extends Thread { 
        public static volatile int a = 0; 
            
        @Override
         public void run() {
             for (int i = 0; i < 10000; i++) {
                 //线程1:取出a的值a=0(被暂停)
                    a++; 
                    //写回
                }
            System.out.println("修改完毕!"); 
         } 
    }
    

    2.测试类:

    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            //1.启动两个线程 
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            t1.start();
            t2.start();
            Thread.sleep(1000);
            System.out.println("获取a最终值:" + MyThread.a.get());
        }
    }
    

    使用原子类对变量操作,无论程序运行多少次,其结果都是正确的!

    2.3 原子类的工作原理

    先来看一下调用过程


    image.png

    在Unsafe类中,调用了一个:compareAndSwapInt()方法,此方法的几个参数:
    var1:传入的AtomicInteger对象
    var2:AtommicInteger内部变量的偏移地址
    var5:之前取出的AtomicInteger中的值;
    var5 + var4:预期结果

    此方法使用了一种"比较并交换(Compare And Swap)"的机制,它会用var1和var2先获取内存AtomicInteger的值,然后和传入的,之前获取的值var5做一下比较,也就是比较当前内存的值和预期的值是否一致,如果一致就修改为var5 + var4,否则就继续循环,再次获取AtomicInteger中的值,再进行比较并交换,直至成功交换为止。
    compareAndSwapInt()方法是"线程安全"的。

    我们假设两个线程交替运行的情况,看看它是怎样工作的:

    初始AtomicInteger的值为0
    线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
    线程A被暂停
    线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
    线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
    线程B成功将AtomicInteger中的值改为1
    线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)

    此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回false,继续循环。
    线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
    线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
    此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。

    CAS机制也被称为:乐观锁。因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会导致CAS失败,而再次循环。

    CAS用作原子操作

    现在CPU内部已经执行原子的CAS操作。Java5以来,你可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能。

    下面是一个使用AtomicBoolean类实现lock()方法的例子:

    1. **public** **static** **class** MyLock { 
    2.   **private** AtomicBoolean locked = **new** AtomicBoolean(**false**); 
    3.   **public** **boolean** lock() { 
    4.     **return** locked.compareAndSet(**false**, **true**); 
    5.   } 
    6. } 
    

    locked变量不再是boolean类型而是AtomicBoolean。这个类中有一个compareAndSet()方法,它使用一个期望值和AtomicBoolean实例的值比较,和两者相等,则使用一个新值替换原来的值。在这个例子中,它比较locked的值和false,如果locked的值为false,则把修改为true。
    如果值被替换了,compareAndSet()返回true,否则,返回false。

    使用Java5+提供的CAS特性而不是使用自己实现的的好处是Java5+中内置的CAS特性可以让你利用底层的你的程序所运行机器的CPU的CAS特性。这会使还有CAS的代码运行更快。

    简单例子:

    1. **public** **static** **void** main(String[] args) { 
    2.  
    3.      AtomicInteger atomicInteger = **new** AtomicInteger(55);//55 
    4.      **int** addAndGet = atomicInteger.addAndGet(23);//78 
    5.      **boolean** compareAndSet = atomicInteger.compareAndSet(78, 43);//true 
    6.      **int** i = atomicInteger.get();//43 
    7.   } 
    
    

    CAS解决原子性问题

    首先,我们设定一个场景,我们有一个数据,初始值为0,我们要将它使用多线程的形式从0累加到10,要求每个线程只能加一次,并且不能出现两个线程累加后的结果出现重复的情况(比如线程A和线程B同时将数据进行累加后变成8),这就不得不考虑原子性为题。

    1.什么是原子性问题?

    原子性问题指的是多线程并发的情况下,当我们要进行数据的读取与修改时,可能会因为线程同时读取数据,导致进行了重复修改的情况,但是我们希望每个线程进行读取修改操作是连贯的、原子性的,在A线程进行读取修改的时候,不允许其他线程进行读取修改,而一定要等到A线程完成修改后,其他线程再去进行读取修改。比如我们要将数字0累加到10,在多线程的情况下,可能会因为线程A在读完数字但是还没有进行修改的时候,线程B也来读了数字,导致线程A的原子性被打破,使得它们累加后得到的数据是一样的,这就破坏了原子性。

    2.为什么用volatile关键字无法保证原子性和互斥性?

    我们都知道volatile关键字修饰的变量是可以保证==内存可见性==的,能保证每次线程修改数据后,其他线程都能知道这个数据被修改了,但是当线程的操作不仅仅是读,变成两步:读+修改,由于volatile无法保证原子性和互斥性,那么就会造成这样的情况:线程A将数据更改为1,线程B和线程C都知道数据变成1了,但是他们是同时对数据1进行再修改,而我们要求每个线程累加后的结果要是不一样的,所以volatile关键字就无法满足我们设定的场景了。

    将数据用volatile修饰,进行线程累加的代码:

    package leo.casTest;
    
    public class CasTest {
        public static void main(String[] args) {
            AtomicThread atomicThread = new AtomicThread();
            // 完成从0加到10的任务需要10个线程
            for (int i = 0; i <= 10; i++) {
                Thread td = new Thread(atomicThread);
                td.start();
            }
        }
    }
    
    // 每个线程的任务就是将数字+1
    class AtomicThread implements Runnable {
        // 我们需要修改的数据,从0累加到10
        private int flagNumber = 0;
    
        @Override
        public void run() {
        
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flagNumber = flagNumber + 1;
            System.out.println("线程: " + Thread.currentThread().getName() + "将数据变为: " + flagNumber);
        
        }
    }
    

    结果:


    image.png

    3.什么是CAS算法?

    CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问,它是一种无锁的非阻塞算法的实现。人话:CAS是靠硬件、底层代码支撑来解决原子性问题的。

    4.CAS算法的过程原理

    CAS有3个东西:

    • 主存中的数据V;
    • 线程拿到的数据A;
    • 线程拟修改的新值B。

    所以过程是这样的,主存里面有一个目前最新的数据假设为1,在我们的场景里,当线程想要对1进行累加的时候会做这么一件事:将线程内部缓存的数据(对应A)假设为1,与主存里面的数据1(对应V)进行比较,因为相等,所以线程就能直接将数据修改为新值2(对应B),这个比较和修改的过程是原子的,不允许其他线程介入。那么如果线程内部的数据缓存假设为1,主存里面的数据假设为2,他们不相等的时候,那么这个线程就不会完成更新,而是会将线程内部缓存的数据(A)变成主存的数据(V)2、并且将拟修改的新值B变成3后,再去和主存里面的值V判断(注意在这个过程中有可能又失败,因为可能其他线程又将主存里面的数据变成3了,就又会逼着线程循环刚才的操作)。


    image.png

    当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。

    5.CAS算法的有哪一些工具类呢?

    java.util.concurrent.atomic 包下提供了一些原子操作的常用类:

    • AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference
    • AtomicIntegerArray 、AtomicLongArray
    • AtomicMarkableReference
    • AtomicReferenceArray
    • AtomicStampedReference

    他们的用法和非原子的是差不多的,会有对应的方法来代替原来的操作。

    核心方法:boolean compareAndSet(expectedValue, updateValue)

    6.使用CAS解决原子性问题

    那么使用CAS的工具类来解决我们将数据从0顺序累加到10:

    实现代码:

    package leo.casTest;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class CasTest {
        public static void main(String[] args) {
            AtomicThread atomicThread = new AtomicThread();
            // 完成从0加到10的任务需要10个线程
            for (int i = 0; i <= 10; i++) {
                Thread td = new Thread(atomicThread);
                td.start();
            }
        }
    }
    
    // 每个线程的任务就是将数字+1
    class AtomicThread implements Runnable {
        // 我们需要修改的数据,从0累加到10,使用CAS工具类生成
        private AtomicInteger flagNumber = new AtomicInteger();
    
        @Override
        public void run() {
        
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 使用方法进行累加
            System.out.println("线程: " + Thread.currentThread().getName() + "将数据变为: " + flagNumber.getAndIncrement());
        }
    }
    

    执行结果:

    image.png

    资料分享:【一键获取】Android核心进阶资料合集

    Android 百大框架源码解析.png pdf大全进阶资料.png

    Android核心技术进阶手册、实战笔记、面试题纲资料

    相关文章

      网友评论

        本文标题:Android并发内存——CAS(CAS原理+原子操作+原子性问

        本文链接:https://www.haomeiwen.com/subject/ijzlirtx.html