美文网首页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