第八章CAS策略

news2024/9/25 17:16:51

文章目录

  • JDK提供的原子类
    • 没有CAS之前
    • 引入CAS之后
  • CAS是什么
    • CAS原理
    • 代码演示
    • 如何保证的原子性
      • 硬件级别保证
    • 源码分析
    • CAS底层原理?如果知道,谈谈你对UnSafe的理解
      • 例子atomicInteger.getAndIncrement()为什么安全
  • 自定义原子引用
  • CAS与自旋锁
      • 实现自旋锁
  • CAS缺点
    • 循环时间长开销大
    • ABA问题
      • JDK的实现
    • 只能保证一个共享变量的原子操作
  • JDK相关原子类使用

JDK提供的原子类

1671202811439.jpg

  • jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

Atomic的原理

  • Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

没有CAS之前

  • 多线程环境不使用原子类保证线程安全i++(基本数据类型)

常用synchronized锁,但是它比较重 ,牵扯到了用户态和内核态的切换,效率不高。

public class CASDemo1 {
	//利用volatile
    private volatile int num;

    public int getNum() {
        return num;
    }

    public synchronized void setNum(int num) {
        num++;
    }
}

引入CAS之后

public class CASDemo1 {
  
    AtomicInteger atomicInteger = new AtomicInteger();
    public int getNum() {
        return atomicInteger.get();
    }

    public void setNum(int num) {
        atomicInteger.getAndIncrement();
    }
}

CAS是什么

  • Compare and Swap比较交换,不会真正的阻塞线程,不断尝试更新,是乐观锁的一种实现方式

  • 中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。

    • 执行CAS操作的时候,将内存位置的值与预期原值比较:

    • 如果相匹配,那么处理器会自动将该位置值更新为新值,

    • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

CAS原理

  • CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
    • 当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
      • 当它重来重试的这种行为成为—自旋!
image-20230615183132937 image-20230615183353242 image-20230615183114477
  • 线程2操作失败,进行自旋操作,重新读取主内存中的值,进行再次重试

代码演示

public class CASDemo2 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,200)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5,300)+"\t"+atomicInteger.get());
    }
}
//true	200
//false 200

如何保证的原子性

  • 那有没有可能我在判断了 线程1的A为0为之后,正准备更新它的新值的时候,被其它线程更改了 i 的值呢?
    • 不会的。因为CAS是⼀种原子操作,它是⼀种系统原语,是⼀条CPU的原⼦指令,从CPU层⾯保证它的原子性
      • 也就是说我们比较和交换这两个操作要么全部成功要么全部失败

硬件级别保证

  • 对总线加锁,效率比synchronized效率高。

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

  • 它是非阻塞的且自身原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作
    • 也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好

你只需要记住:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性

实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令。

核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来。

源码分析

//compareAndSet
//发现它调用了Unsafe类
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//compareAndSwapInt
//发现它调用了native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这三个方法是类似的
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面三个方法都是类似的,主要对4个参数做一下说明。

  • var1:表示要操作的对象
  • var2:表示要操作对象中属性地址的偏移量
  • var4:表示需要修改数据的期望的值
  • var5/var6:表示需要修改为的新值

引出来一个问题:Unsafe类是什么?

CAS底层原理?如果知道,谈谈你对UnSafe的理解

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();
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;//保证变量修改后多线程之间的可见性
}
  • 天上飞的理念,必有落地的实现

    • CAS这个理念 ,落地就是Unsafe类
  • 它是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门 ,基于该类可以直接操作特定内存的数据 。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

    • 注意Unsafe类中的所有方法都是 native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务 。

变量valueOffset

  • 表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));

变量value用volatile修饰

  • value值用volatile是为了实现可见性,能让线程能够实时获得主内存的最新值来进行比较

例子atomicInteger.getAndIncrement()为什么安全

  • CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
    • 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
  • AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

image-20230703102558402

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令 。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语 ,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

  • var5是表示需要修改数据的期望的值,是提高getIntVolatile()获得的,一看方法名就知道是获得主内存最新的值
  • compareAndSwapInt就是操作系统提供的原子操作CAS
    • 如果我们的期望值(刚刚获取的var5)跟主内存中值相同,则进行修改,返回true,取反就退出循环
      • 通过C语言指针类似,直接操作内存获取对应的数据,var1是this,var2是偏移地址
    • 如果我们的期望值跟主内存中的值不同,则返回false,取反就继续循环,再次尝试

自定义原子引用

  • 譬如AtomicInteger原子整型,可否有其他原子类型?比如AtomicBook、AtomicOrder*
  • 可以!
  • 丢入泛型中Class AtomicReference<V>
@Getter
@ToString
@AllArgsConstructor
class User{
    String userName;
    int age;
}
public class CASDemo3 {
    public static void main(String[] args) {
        User zhangSan = new User("zhangsan", 20);
        User liSi = new User("lisi", 22);
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        userAtomicReference.set(zhangSan);
        System.out.println(userAtomicReference.compareAndSet(zhangSan,liSi)+"\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(zhangSan,liSi)+"\t"+userAtomicReference.get().toString());
    }
}
true	User(userName=lisi, age=22)
false	User(userName=lisi, age=22)

CAS与自旋锁

自旋锁(spinlock)

  • 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁 ,

  • 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

若在OpenJDK源码中查看Unsafe.java

 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;
    }
  • 这里while体现了自旋的思想

  • 假如是ture,取反false退出循环;假如是false,取反true要继续循环。

实现自旋锁

题目:实现一个自旋锁
自旋锁好处:循环比较获取没有类似wait的阻塞。

通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。

public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"尝试获取锁");
        //只有没有线程占用的时候,才能加锁
        while (!atomicReference.compareAndSet(null,thread)){

         }
        //如果是空的,那么就吧thread放进去
        System.out.println(Thread.currentThread().getName()+"\t"+"获得了这个自旋锁");
    }
    public void unLock(){
        Thread thread = Thread.currentThread();
        //只有当前占用锁的线程才能进行解锁
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t"+"-------任务完成,解锁.....");
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        new Thread(()->{
            spinLock.lock();
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLock.unLock();
        },"t1").start();
        new Thread(()->{
            spinLock.lock();
            spinLock.unLock();
        },"t2").start();
    }
}
t1	尝试获取锁
t1	获得了这个自旋锁
t2	尝试获取锁
t1	-------任务完成,解锁.....
t2	获得了这个自旋锁
t2	-------任务完成,解锁.....
  • while (!atomicReference.compareAndSet(null,thread))
    • 相当于CAS(V,A,B),V表示当前锁的拥有者,也就是占用锁的线程 A(null)表示希望当前没有线程拥有这个锁 B表示当前的执行这个方法线程
      • 当ANULL的时候,表示当前自旋锁没有被任何线程拥有,尝试将this.ownerThread.currerntThread() ,将持有锁的对象变成当前线程

CAS缺点

  • ABA 问题

  • 循环时间长开销大

  • 只能保证一个共享变量的原子操作

循环时间长开销大

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

do while 如果它一直自旋会一直占用CPU时间,造成较大的开销

  • 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
    • 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
img

解决的方法

  • 在ABA问题中引入版本号
    • 当CAS V==A的时候,才会用到版本号,来判断当前的主内存的V是否被别的线程反复修改过

JDK的实现

AtomicStampedReference版本号 (注意区分前面的Class AtomicReference<V>

AtomicStampedReference(V initialRef, int initialStamp)
创建一个新的 AtomicStampedReference与给定的初始值。

ABA问题复现

public class CASDemo4 {
    static AtomicReference atomicReference = new AtomicReference<>(100);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get());
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(101, 100)+"\t"+atomicReference.get());
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(100, 103)+"\t"+atomicReference.get());
        },"t2").start();
    }
}
t1操作true	101
t1操作true	100
t2操作true	103

添加版本号

public class CASDemo4 {
    static AtomicReference atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp+"\t"+"数值为"+atomicStampedReference.getReference());//1-----------初始获得一样的版本号
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(100, 101,1,2)+"\t"
                    +"第二次操作版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(101, 100,2,3)+"\t"+
                    "第三次操作版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(100, 103,1,2)+"\t"+
                    "第四次版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
        },"t2").start();
    }
}
t1	 首次版本号:1	数值为100
t1操作	true	第二次操作版本号为2	数值为101
t1操作	true	第三次操作版本号为3	数值为100
t2操作	false	第四次操作版本号为3	数值为100

只能保证一个共享变量的原子操作

  • 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

JDK相关原子类使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/712732.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SD卡可以数据恢复吗 SD卡数据恢复软件

随着数码设备的普及和存储容量的不断增加&#xff0c;SD卡已经成为我们常用的存储介质之一。在使用过程中&#xff0c;不可避免地会遇到SD卡数据丢失的情况&#xff0c;可能是因为误操作、格式化、病毒感染或物理损坏等。幸运的是&#xff0c;现在有许多专业的SD卡数据恢复软件…

Guava RateLimiter:原理、源码和思想

Guava RateLimiter&#xff1a;原理、源码和思想 三种限流算法计数器法漏桶算法令牌桶算法 RateLimiterRateLimiter的使用RateLimiter原理RateLimiter获取令牌获取令牌的基本流程获取令牌的详细源码解读 RateLimiter的两种限流器试验&#xff1a;感受两种限流器的区别SmoothBur…

Mysql 事务优化

事务的ACID特性: 原子性(Atomicity):当前事务的操作要么同时成功&#xff0c;要么同时失败。原子性由undo log日志来保证 一致性(Consistency):使用事务的最终目的&#xff0c;由业务代码正确逻辑保证 隔离性(lsolation):在事务并发执行时&#xff0c;他们内部的操作不能互相…

Ai 制作简单的iconfont字体图标

打开网址https://www.iconfont.cn/icons/upload?spma313x.7781069.1998910419.d059fa781 下载矢量图标设计模板,如图 打开下载的图标制作模版.ai文件 点击椭圆工具画一个大小为900*900的圆 点击文字工具&#xff0c;输入文字内容并调整为合适的大小&#xff0c;例610px …

欧科云链OKLink:2023年6月安全事件盘点

一、基本信息 2023年6月REKT和RugPull事件约造成约1000万美元损失&#xff0c;相比上月有显著下降&#xff0c;但安全事件发生数量依旧不减。其中被废弃的Atlantis Loans遭受治理攻击&#xff0c;造成的损失超250万美元&#xff0c;对该协议有过授权的用户需及时撤销授权。另外…

GRU、LSTM、注意力机制(第八次组会)

GRU、LSTM、注意力机制(第八次组会) 一、 GRU二、 LSTM三、 深度RNN、双向RNN四、 注意力机制一、 GRU 二、 LSTM 三、 深度RNN、双向RNN

机器学习实验-决策树、朴素贝叶斯、KNN模型的应用

实验 决策树-西瓜数据集基于基尼系数剪枝人体运动状态预测数据集西瓜数据集人体特征数据集 机器学习 数据集有两个&#xff0c;一个是西瓜数据集&#xff0c;另一个是人体特征数据集 决策树-西瓜数据集 基于信息熵准则进行划分选择的决策树算法的实现过程(不使用现有的机器学习…

有必要使用性能测试工具吗?

有必要使用性能测试工具吗&#xff1f;性能测试工具是一种帮助开发人员和测试人员评估应用程序、网站或系统性能的软件。这些工具主要用于测量响应时间、吞吐量、并发用户数、资源利用率等指标&#xff0c;以及发现系统瓶颈和瓶颈原因&#xff0c;那其作用意义有哪些&#xff1…

Flutter Linux desktop桌面版Build App流程

Flutter Linux desktop桌面版Build App流程 Flutter 是 Google推出并开源的移动应用开发框架&#xff0c;主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App&#xff0c;一套代码同时运行在 iOS 和 Android平台。 Flutter提供了丰富的组件、接口&#xff0c;开发者…

maven-mvnd 的使用

maven-mvnd是Apache Maven团队借鉴了Gradle和Takari的优点&#xff0c;衍生出来的更快的构建工具&#xff0c;是maven的强化版。 github地址&#xff1a;https://github.com/apache/maven-mvnd maven-mvnd 特性&#xff1a; 嵌入 Maven (所以不需要单独安装 Maven)&#xff…

【kubernetes系列】kubernetes之initcontainer初始化容器

概述 Init Container就是用来做初始化工作的容器&#xff0c;可以是一个或者多个&#xff0c;如果有多个的话&#xff0c;这些容器会按定义的顺序依次执行&#xff0c;只有所有的Init Container执行完后&#xff0c;主容器才会被启动。我们知道一个Pod里面的所有容器是共享数据…

日本PSE认证341种B类产品为非特定电器和材料类清单

以上清单为官方网站直译&#xff0c;可能存在差异&#xff0c;具体以官方为准。

为什么普通人升级电脑的意愿消逝了

简洁版答案&#xff1a;需求消逝了。 市场调研机构Canalys数据显示&#xff0c;今年一季度&#xff0c;中国市场整体PC出货量同比下降24%至890万台&#xff0c;已是连续第五个季度下跌。今年截至618结束&#xff0c;都没有一家主要的PC厂商愿意发战报。PC市场怎样走出寒冬&…

微服务系列文章之 seata 事务模式

XA模式 XA 规范 是 X/Open 组织定义的分布式事务处理&#xff08;DTP&#xff0c;Distributed Transaction Processing&#xff09;标准。 XA 规范 描述了全局的TM与局部的RM之间的接口&#xff0c;几乎所有主流的数据库都对 XA 规范 提供了支持。 两阶段提交 XA是规范&…

若依新建模块后,swagger扫描不到新模块中的controller接口

框架默认情况下,只能扫描ruoyi-admin模块下的controller接口。如果需要扫描其它子模块中的swagger注解,那么需要调整一下swagger相关内容。 目录 1、前置条件 2、解决办法 1、前置条件 首先,我们要知道,默认情况下swagger依赖包是放置在项目根pom.xml文件中的。 swagge…

Redis三大集群模式

Redis 支持三种集群方案 主从复制模式Sentinel&#xff08;哨兵&#xff09;模式Cluster 模式 为什么要使用集群模式 [1]解决单机故障问题 [2]解决单机压力问题 Redis 集群的三种模式&#x1f349; 一.主从复制模式&#x1f349; 主从复制原理&#x1f95d; 从服务器连接…

【软件测试】高级测试进阶 Centos7安装 Docker容器(详细步骤)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 OS 安装环境要求 …

移动应用加固:政务APP如何保障安全?

政务APP是政府部门为了方便公民办理政务事务而开发的移动应用程序。由于政务APP涉及到大量的敏感信息和政府机密&#xff0c;因此面临着多种安全风险的挑战。《国务院办公厅关于印发全国一体化政务服务平台移动端建设指南的通知》&#xff08;以下简称“通知”&#xff09;要求…

JAVA开发运维(linux环境防火墙与端口开启使用总结记录)

一、问题背景&#xff1a; 将web项目开发完成&#xff0c;需要上到生产环境。那么我们应用调用的一些ip&#xff0c;端口都是要设置的&#xff0c;比如说应用提供给谁访问&#xff0c;那些人不能访问&#xff0c;应用的端口是多少&#xff0c;也是需要开启才能访问的。在实际研…

用BBI指标如何分析平台现货白银走势?

分析平台现货白银走势&#xff0c;主要有几种理论&#xff0c;包括指标类&#xff0c;价格形态类&#xff0c;切线类和K线理论类等几种方法。其实&#xff0c;技术指标依赖投资者开发的算法&#xff0c;被认为是相对更先进的技术&#xff0c;所以也深受很多投资者喜爱。今日&am…