JUC并发编程第九篇,原子操作类分类解析,LongAdder为什么这么快原理分析?

news2024/12/28 20:23:34

JUC并发编程第九篇,原子操作类分类解析,LongAdder为什么这么快原理分析?

    • 一、基本类型原子类
    • 二、数组类型原子类
    • 三、引用类型原子类
    • 四、对象的属性修改原子类
    • 五、原子操作增强类
    • 六、原理分析,LongAdder 为什么这么快?

  • 位于 java.base 模块, java.util.concurrent.atomic 工具包,它支持对单个变量进行无锁线程安全编程。
    在这里插入图片描述

一、基本类型原子类

可以原子方式更新的 int,boolean,long值,基本类型原子类包括三个:AtomicInteger、AtomicBoolean、AtomicLong

常用API如下:

  • public final int get(); 获取当前的值
  • public final int getAndSet(int newValue); 获取当前的值,并设置新的值
  • public final int getAndIncrement(); 获取当前的值,并自增
  • public final int getAndDecrement(); 获取当前的值,并自减
  • public final int getAndAdd(int delta); 获取当前的值,并加上预期的值
  • boolean compareAndSet(int expect, int update); 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

举例:AtomicInteger

class MyNumber{

    private AtomicInteger atomicInteger = new AtomicInteger();

    public void addPlus(){
        atomicInteger.incrementAndGet();
    }

    public AtomicInteger getAtomicInteger() {
        return atomicInteger;
    }
}

public class AtomicIntegerDemo {
    //100个线程,每个线程加5000次,CountDownLatch 等待上一个线程执行完毕
    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(100);

        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 5000; j++) {
                        myNumber.addPlus();
                    }
                }finally {
                    countDownLatch.countDown();
                }

            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println(myNumber.getAtomicInteger().get());
    }
}

二、数组类型原子类

一个数组,其中元素可以原子方式更新,数组类型原子类包括三个:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

常用API如下:

  • accumulateAndGet​(int i, int x, IntBinaryOperator accumulatorFunction); 原子更新索引 i处的元素以及将给定函数应用于当前值和给定值的结果,返回更新的值。
  • addAndGet​(int i, int delta); 原子地将给定值添加到索引 i 处的元素。
  • compareAndExchange​(int i, int expectedValue, int newValue); 如果元素的当前值等于期望值,原子方式将 i 索引处的元素设置为 newValue。
  • getAndIncrement​(int i); 原子地增加索引 i处元素的值。
  • getAndSet​(int i, int newValue); 以原子方式设置索引 i 处值为 newValue ,并返回原来的值。

举例:AtomicIntegerArray

public class AtomicIntegerArrayDemo {

    public static void main(String[] args) {

        //三种初始化方式
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});

        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.print(atomicIntegerArray.get(i) + "  ");
        }
        System.out.println();

        int tmpInt = 0;
        tmpInt = atomicIntegerArray.getAndSet(0, 1111);
        System.out.println(tmpInt+"\t"+atomicIntegerArray.get(0));

        atomicIntegerArray.getAndIncrement(1);
        atomicIntegerArray.getAndIncrement(1);
        tmpInt = atomicIntegerArray.getAndIncrement(1);
        System.out.println(tmpInt+"\t"+atomicIntegerArray.get(1));
    }
}

在这里插入图片描述

三、引用类型原子类

AtomicReference : 可以原子方式更新的对象引用。

class User{
    String userName;
    int    age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3 = new User("shangsan", 24);
        User l4 = new User("lisi", 28);

        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(z3);

        System.out.println(atomicReference.compareAndSet(z3,l4)+"\t"+atomicReference.get().toString());

        System.out.println(atomicReference.compareAndSet(z3,l4)+"\t"+atomicReference.get().toString());
    }
}

在这里插入图片描述
AtomicStampedReference : 维护一个对象引用以及一个整数“标记”,可以原子方式更新。
携带版本号的引用类型原子类,可以解决ABA问题,可以记录修改过几次

public class ABADemo {

    static AtomicInteger atomicInteger = new AtomicInteger(100);
    //初始标记1
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args) {
        abaProblem();
        abaResolve();
    }

    public static void abaResolve() {
        //过程:t3打印初始标记 -》 t4打印初始标记 -》 t3 ABA -》 t4 compareAndSet 失败 (代码里边的sleep为了保证代码执行顺序)
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t3 ----第1次stamp  "+stamp);
            
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            
            atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
            System.out.println("t3 ----第2次stamp  "+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println("t3 ----第3次stamp  "+atomicStampedReference.getStamp());
            
        },"t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("t4 ----第1次stamp  "+stamp);
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            
            boolean result = atomicStampedReference.compareAndSet(100, 222222, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
        },"t4").start();
    }

    public static void abaProblem() {
        //t1线程:A -> B -> A 
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
        //t2线程:不知道
        new Thread(() -> {
            atomicInteger.compareAndSet(100,22222222);
            System.out.println(atomicInteger.get());
        },"t2").start();
    }
}

AtomicMarkableReference : 维护一个对象引用以及一个标记位,可以原子方式更新。
一次性的,只能记录是否修改过,因为它将状态标记简化为了Boolean 的 true/false

public class MarkableReferenceDemo {

    static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100,false);

    public static void main(String[] args) {
        //AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过
        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);

            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }

            markableReference.compareAndSet(100,101,marked,!marked);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+markableReference.isMarked());
            markableReference.compareAndSet(101,100,markableReference.isMarked(),!markableReference.isMarked());
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+markableReference.isMarked());
        },"t1").start();

        new Thread(() -> {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
            //暂停几秒钟线程
            try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }

            markableReference.compareAndSet(100,2020,marked,!marked);
            System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference()+"\t"+markableReference.isMarked());
        },"t2").start();
    }
}

四、对象的属性修改原子类

  • 使用目的:以一种线程安全的方式操作非线程安全对象内的某些字段。
  • 使用要求:1、更新的对象属性必须使用 public volatile 修饰符。 2、因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

AtomicIntegerFieldUpdater 原子更新对象中int类型字段的值
AtomicLongFieldUpdater 原子更新对象中Long类型字段的值

class BankAccount {
    String bankName = "CCB";

    //以一种线程安全的方式操作非线程安全对象内的某些字段

    //1 更新的对象属性必须使用 public volatile 修饰符。
    public volatile int money = 0;

    //2 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
    AtomicIntegerFieldUpdater FieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");

    public void transfer(BankAccount bankAccount) {
        FieldUpdater.incrementAndGet(bankAccount);
    }
}

public class AtomicIntegerFieldUpdaterDemo {
    public static void main(String[] args) throws InterruptedException {
        BankAccount bankAccount = new BankAccount();

        for (int i = 1; i <= 1000; i++) {
            new Thread(() -> {
                bankAccount.transfer(bankAccount);
            },String.valueOf(i)).start();
        }

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        
        System.out.println(Thread.currentThread().getName()+"\t"+"---bankAccount: "+bankAccount.money);
    }
}

AtomicReferenceFieldUpdater 原子更新引用类型字段的值

/**
 * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
 */
class MyVar {

    public volatile Boolean isInit = Boolean.FALSE;

    AtomicReferenceFieldUpdater<MyVar,Boolean> FieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");

    public void init(MyVar myVar) {

        if(FieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName()+"\t"+"---start init");

            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

            System.out.println(Thread.currentThread().getName()+"\t"+"---end init");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"---抢夺失败,已经有线程在修改中");
        }
    }
}

public class AtomicReferenceFieldUpdaterDemo {
    public static void main(String[] args) {

        MyVar myVar = new MyVar();

        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}

五、原子操作增强类

包括:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder
在这里插入图片描述

演示(LongAccumulator,LongAdder)

  • LongAdder只能用来计算加法,且从零开始计算,LongAccumulator提供了自定义的函数操作
public class LongAdderAPIDemo {
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder();

        longAdder.increment();
        longAdder.increment();
        longAdder.increment();

        System.out.println(longAdder.longValue());
        //=======================================
        
        LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x * y ,2);

        longAccumulator.accumulate(1);
        longAccumulator.accumulate(2);
        longAccumulator.accumulate(3);

        System.out.println(longAccumulator.longValue());
    }
}
  • 性能对比:
class ClickNumberNet {
    int number = 0;
    public synchronized void clickBySync(){
        number++;
    }

    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong(){
        atomicLong.incrementAndGet();
    }

    LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x + y,0);
    public void clickByLongAccumulator(){
        longAccumulator.accumulate(1);
    }

    LongAdder longAdder = new LongAdder();
    public void clickByLongAdder(){
        longAdder.increment();
    }

}


public class LongAdderDemo{
    public static void main(String[] args) throws InterruptedException {
        ClickNumberNet clickNumberNet = new ClickNumberNet();

        long startTime;
        long endTime;

        CountDownLatch countDownLatch1 = new CountDownLatch(50);
        CountDownLatch countDownLatch2 = new CountDownLatch(50);
        CountDownLatch countDownLatch3 = new CountDownLatch(50);
        CountDownLatch countDownLatch4= new CountDownLatch(50);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <= 50 ; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 100 * 10000; j++) {
                        clickNumberNet.clickBySync();
                    }
                }finally {
                    countDownLatch1.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickBySync result: "+clickNumberNet.number);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=50; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * 10000; j++) {
                        clickNumberNet.clickByAtomicLong();
                    }
                }finally {
                    countDownLatch2.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByAtomicLong result: "+clickNumberNet.atomicLong);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=50; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * 10000; j++) {
                        clickNumberNet.clickByLongAccumulator();
                    }
                }finally {
                    countDownLatch3.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAccumulator result: "+clickNumberNet.longAccumulator.longValue());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <=50; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=100 * 10000; j++) {
                        clickNumberNet.clickByLongAdder();
                    }
                }finally {
                    countDownLatch4.countDown();
                }
            },String.valueOf(i)).start();
        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println("----costTime: "+(endTime - startTime) +" 毫秒"+"\t clickByLongAdder result: "+clickNumberNet.longAdder.sum());

    }
}

在这里插入图片描述

六、原理分析,LongAdder 为什么这么快?

在这里插入图片描述
和其他不同的是,LongAdder是Striped64的子类,Striped64有两个重要的属性:
在这里插入图片描述
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

在这里插入图片描述
LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。

使用总结

  • AtomicLong:线程安全,可允许一些性能损耗,要求高精度时可使用,AtomicLong是多个线程针对单个热点值value进行原子操作。
  • LongAdder:当需要在高并发下有较好的性能表现,且对值的精确度要求不高时可以使用,LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。

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

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

相关文章

JS获取音频的总时长,解决Audio元素duration为NaN || Infinity 问题

当我们在加载一个线上mp3地址或者获取audio的duration的时候&#xff0c;会发现有拿到duration是Infinity的情况&#xff0c;这时如果我们动态的展示录音时间时候就会有问题。首先明确一下这是chrome浏览器自己的存在的一个bug&#xff0c;因为我们拿到的录音数据流没有定义长度…

商务与经济统计 | 推断统计

一.概率 事件 若干样本点的集合 事件的概率 等于事件中所有的样本点概率之和 条件概率 贝叶斯定理 二.离散型概率分布 随机变量 是一次试验的结果的数值性描述 离散型随机变量 指的是有穷个数值或一系列无穷的数值的随机变量 连续型随机变量 代表某一区间或多个区间…

通配符的应用

我们使用通配符描述切入点&#xff0c;主要的目的就是简化之前的配置&#xff0c;具体都有哪些通配符可以使用? *:单个独立的任意符号&#xff0c;可以独立出现&#xff0c;也可以作为前缀或者后缀的匹配符出现 execution&#xff08;public * com.itheima.*.UserService.find…

webpack基础配置教程

文章目录1.初识Webpack2.开启项目3.处理js和json文件webpack小试牛刀webpack打包js/json文件webpack默认不能处理css4.webpack配置文件1.初识Webpack 什么是webpack? Webpack是一个模块打包器&#xff08;意思同构建工具&#xff0c;所谓构建︰将程序员写完的【源代码】&#…

Sentinel服务熔断功能

Sentinel服务熔断功能 sentinel整合ribbonopenFeignfallback 1、环境搭建&#xff08;新建模块&#xff09; 1.1、启动nacos和sentinel 1.2、新建服务提供者cloudalibaba-provider-payment9003/9004模块 1、引入pom.xml文件 <?xml version"1.0" encoding&quo…

21.前端笔记-CSS-字体图标

1、字体图标产生 使用场景&#xff1a;用于显示网页中通用的小图标iconfont 为什么不用精灵图&#xff1a; &#xff08;1&#xff09;图片文件还是比较大的 &#xff08;2&#xff09;图片本身放大或缩小会失真 &#xff08;3&#xff09;一旦图片制作完毕想要更换&#xf…

ThingsBoard 3.1.1版本在window本地运行之设备直连(二)

目录 前言 1、Thingsboard 框架 2、MQTT设置 1.MQTT概念 2.MQTT在TB里担任的角色 3.MQTT配置 3、结果 前言 ThingsBoard是一个物联网管理平台&#xff0c;这个平台可以让其他企业入驻进来&#xff0c;这些入驻的企业或者个人就是租户&#xff08;tenant&#xff09;&#…

入门系列 - Git工作流程

Git工作流程 Git的工作流程一般如下&#xff1a; 克隆 Git 资源作为工作目录。在克隆的资源上添加或修改文件。如果其他人修改了&#xff0c;你可以更新资源。在提交前查看修改。提交修改。在修改完成后&#xff0c;如果发现错误&#xff0c;可以撤回提交并再次修改并提交。 …

【C++】vector

vector与string许多功能相似&#xff0c;有了string的基础学起来很轻松 文章目录一、vector的介绍二、vector的使用1、vector定义&#xff08;构造&#xff09;类2、vector与string相似的接口3、vector迭代器失效问题三、vector的模拟实现一、vector的介绍 vector文档&#xf…

第十四届蓝桥杯集训——JavaC组第七篇——逻辑运算符

第十四届蓝桥杯集训——JavaC组第七篇——逻辑运算符 目录 第十四届蓝桥杯集训——JavaC组第七篇——逻辑运算符 逻辑运算符 逻辑与 逻辑或 非 逻辑运算法优先级 练习题&#xff1a; 逻辑运算符 &&逻辑与‖逻辑或!逻辑非逻辑运算与位运算不同&#xff0c;逻辑运…

脚本语言Bash简明教程【1】

Bash(GNU Bourne-Again Shell)is a Unix shell and script language written by Brian Fox for the GNU Project as a free software replacement for the Bourne shell. First released in 1989, it has been used as the default login shell for most Linux distributions…

React - Context 使用(共享对于一个组件树而言是 “全局” 的数据)

React - Context 使用&#xff08;共享对于一个组件树而言是 “全局” 的数据&#xff09;一. Context 概念理解二. Context 使用三. Context 组件传值实例Context官网&#xff1a; https://zh-hans.reactjs.org/docs/context.html 一. Context 概念理解 Context 提供了一个无…

Redis缓存雪崩、击穿、穿透、双写一致性、并发竞争、热点key重建优化、BigKey的优化 等解决方案

一. 缓存雪崩 1. 含义 同一时刻&#xff0c;大量的缓存同时过期失效。 2. 产生原因和后果 (1). 原因&#xff1a;由于开发人员经验不足或失误&#xff0c;大量热点缓存设置了统一的过期时间。 (2). 产生后果&#xff1a;恰逢秒杀高峰&#xff0c;缓存过期&#xff0c;瞬间海…

核心竞争力决定未来,中国社会科学院与美国杜兰大学金融管理硕士项目为你助力

随着社会发展的日新月异&#xff0c;知识更新迭代更是以秒来计算&#xff0c;我们不得不为自身有限的技能和认知而焦虑。面对新的机遇最好的应对方法就是要有学习能力&#xff0c;永远学习&#xff0c;终身学习&#xff0c;这是别人永远都抢不走的竞争力。身在金融领域的你&…

关于flex布局和九宫格布局的实现

1.父容器常见属性 display&#xff1a;flex (项目在主轴上的排列方式) justify-content&#xff1a;flex-start / flex-end / center / space-around / space-between &#xff08;项目在交叉轴上的排列方式&#xff09; align-items: flex-start / flex-end / center / ba…

类实现接口,并且对象转型引用,接口引用实现类,抽象类实现接口且被子类继承返回抽象类的值给接口对象转型父类引用子类

类实现接口&#xff0c;并且对象转型引用 目录接口&#xff1a;最最特殊的抽象类。声明行为当多个类有共同的属性和方法用抽象类当符合什么是什么&#xff1f;用继承&#xff1b;继承抽行类当描述能干嘛&#xff1f;用接口接口的方法都是抽象方法的声明接口和抽象类的区别以人的…

集成springSecurity遇到的跨域问题

引言 该项目主要使用技术&#xff1a;sprinboot、springSecurity、vue,其它的技术就不介绍了 其中springSecurity是我参考网上的案例去进行的集成&#xff0c;虽然集成成功了&#xff0c;但是还不是太懂。 下面就开始介绍一下我遇到的问题 问题重现 由于我项目后端集成了s…

[附源码]Python计算机毕业设计Django自行车租赁管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

【LSTM时序预测】基于LSTM实现时间序列神经网络预测附MATLAB代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

行话|入局「软件定义汽车」,你真的准备好了吗?

什么是行话&#xff1f; 「行话」&#xff0c;是极狐 GitLab 推出的全新内容系列&#xff0c;探讨 Git 与 DevOps 在不同行业的实践场景与解决方案&#xff0c;希望能够为不同行业的软件开发者带来一些全新的思考和输入。 说行业&#xff0c;讲行话。 这一期&#xff0c;我们…