【多线程与高并发 四】CAS、Unsafe 及 JUC 原子类详解

👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列、MySQL系列、Redis系列、Leetcode算法系列、GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦❤️
✨时间是条环形跑道,万物终将归零,亦得以圆全完美

CAS、Unsafe 及 JUC 原子类详解

    • CAS
      • 自旋等待
      • 无法保证多变量原子性
      • ABA 问题
    • Unsafe
      • Unsafe 之 CAS
        • 典型应用
      • 内存操作
        • 典型应用
      • 线程调度
    • Atomic 原子类
      • 基本类型
      • 数组类型
      • 引用类型
      • 对象的属性修改类型


多线程及高并发系列

  • 【多线程及高并发 一】内存模型及理论基础
  • 【多线程及高并发 二】线程基础及线程中断同步
  • 【多线程及高并发 三】volatile & synchorized 详解
  • 【多线程及高并发 番外篇】虚拟线程怎么被 synchronized 阻塞了?

在【多线程及高并发 一】内存模型及理论基础中说到多线程的原子性,i++的操作其实需要三条 CPU 指令:

  1. 将变量 i 从内存读取到 CPU寄存器
  2. 在CPU寄存器中执行 i + 1 操作
  3. 将最后的结果 i 写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)

为了解决原子性问题,保证线程安全,可以使用synchronizedReentrantLock来进行线程互斥同步,也可以使用基于CAS实现的java.util.concurrent.atomic包中原子类进行无锁的非阻塞同步

ReentrantLockAQS的一种实现,其底层操作其实也是使用CAS+volatile

CAS

在Java中,CASCompare and Swap)是一种并发编程技术,用于实现多线程环境下的无锁同步,由于不断循环重试也被称为自旋锁CAS操作包括三个操作数:内存位置(通常是一个变量)、预期原值和新值

CAS操作会比较内存位置的当前值与预期原值是否相等,如果相等,则将该内存位置的值更新为新值;如果不相等,则不做任何操作

CAS操作是一种乐观锁技术,它可以避免使用传统锁带来的性能开销和线程阻塞。然而,需要注意的是,CAS操作并不适用于所有并发场景,特别是在存在大量线程竞争的情况下,CAS操作可能会导致自旋等待,降低性能

CAS操作的简单示例

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        // 启动多个线程进行自增操作
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                int oldValue, newValue;
                do {
                    // 获取当前值
                    oldValue = counter.get();
                    // 计算新值
                    newValue = oldValue + 1;
                    // 使用CAS操作尝试更新值
                } while (!counter.compareAndSet(oldValue, newValue));
            }).start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终结果
        System.out.println("Counter: " + counter.get());
    }
}

相对于互斥同步的synchronizedCAS操作是一种乐观锁技术,它可以避免使用传统锁带来的性能开销和线程阻塞。然而使用CAS操作可能会导致自旋等待、ABA问题、无法保证多变量原子性等问题

自旋等待

当多个线程同时尝试执行CAS操作时,如果某个线程的CAS操作失败,它会一直尝试执行CAS直到成功为止,这个过程称为自旋等待。自旋等待会消耗CPU资源,降低系统的整体性能

无法保证多变量原子性

虽然CAS操作是原子的,但它只能保证单个变量的原子性操作。如果需要对多个变量进行原子操作,就需要使用其他同步机制如锁

Java 1.5 后,JDK提供了AtomicReference来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

ABA 问题

ABA问题是指在CAS(Compare and Swap)操作中可能出现的一种情况,其中一个变量的值在开始时是A,然后被改变为B,最后又被改回A。这样的变化序列可能导致CAS操作错误地认为期间没有发生变化,但实际上变化了

为了解决ABA问题,可以使用版本号或标记来追踪变化过程。每次对共享变量进行修改时,都会增加一个版本号或标记,以便在CAS操作中比较变化过程是否符合预期。使得A->B->A 变为 1A->2B->3A

对于常量类型来说,ABA问题可能不一定会影响预期,但是对于对象来说期间被修改过其实不符合预期

AtomicStampedReference类是 Java 中提供的一个用于解决ABA问题的原子类。它通过引入版本号(stamp)来追踪共享变量的变化过程,从而在CAS操作中比较变化过程是否符合预期

Unsafe

Unsafe是Java中的一个特殊类,它提供了直接操作内存和执行低级别操作的功能。虽然它是Java的内部API,不建议在普通应用程序中直接使用它,但它在Java的核心库和一些高级框架中被广泛使用

官方建议开发人员避免直接使用Unsafe类,而是使用 Java 提供的更高级别的并发和内存管理工具,如java.util.concurrent包和java.nio包中的类

Unsafe类的主要功能和特点:

  • 直接内存操作:Unsafe允许直接操作堆外内存,即绕过Java虚拟机的内存管理机制,直接操作内存的原始字节。这种能力对于优化和处理大量数据的高性能应用程序非常有用。
  • 数组操作:Unsafe提供了一些方法来操作数组,例如在数组中获取和设置元素的值,以及进行数组的复制和填充等操作。
  • 对象操作:Unsafe允许直接操作对象的字段,包括获取和设置字段的值,以及对字段进行原子更新等操作。它可以绕过Java语言中的访问权限控制,对私有字段进行访问和修改。
  • 内存屏障和原子操作:Unsafe提供了内存屏障(Memory Barriers)和原子操作的支持,用于在多线程环境下实现并发控制和同步。
  • 类加载和实例化:Unsafe提供了一些方法来加载和实例化类,包括分配类的实例和操作类的静态字段

Unsafe

Unsafe 之 CAS

如下源代码释义所示,这部分主要为CAS相关操作的方法。

/**
    *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
  
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

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

典型应用

CASjava.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用

如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过UnsafeobjectFieldOffset方法获取。
AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作

下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress=“0x110000”,通过baseAddress + valueOffset得到value的内存地址valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止

内存操作

这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法

//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

通常,我们在Java中创建的对象都处于堆内内存中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存

与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法

典型应用

DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty 、MINA 等 NIO 框架中应用广泛。

  • Direct Buffer 可以通过ByteBuffer.allocateDirect()方法来创建,它的数据存储在堆外内存中,生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多 IO 操作会很高效
  • 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高

Direct Buffer 创建和销毁过程中,都会比一般的堆内 Buffer 增加部分开销,所以通常都建议用于长期使用、数据较大的场景

DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存 API 来实现

上图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放

线程调度

包括线程挂起、恢复、锁机制等方法

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

如上源码说明中,方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常

Java 锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现

更多Unsafe相关功能,详见Java魔法类:Unsafe应用解析|美团技术团队

Atomic 原子类

JDK 提供了基于CAS实现的一系列原子类,在java.util.concurrent.atomic包下

根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类

基本类型
使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型
使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型。该类提供了一个布尔标记(mark)来表示共享变量的变化,而无法追踪具体的变化过程
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题

对象的属性修改类型

原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

以 AtomicInteger举例,AtomicInteger 类常用方法

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)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public class AtomicIntegerExample {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger(0);

        // 增加计数器的值
        counter.incrementAndGet();
        System.out.println("增加后的值: " + counter.get());

        // 减少计数器的值
        counter.decrementAndGet();
        System.out.println("减少后的值: " + counter.get());

        // 使用addAndGet方法增加指定的值
        counter.addAndGet(5);
        System.out.println("增加后的值: " + counter.get());

        // 使用getAndAdd方法获取当前值并增加指定的值
        int previousValue = counter.getAndAdd(10);
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + counter.get());
    }
}

数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

以 AtomicIntegerArray 举例,AtomicIntegerArray 类常用方法

public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicIntegerArray 类使用示例:

public class AtomicIntegerArrayExample {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        AtomicIntegerArray atomicArray = new AtomicIntegerArray(array);

        // 原子地增加指定索引位置的元素值
        atomicArray.getAndIncrement(2); // 索引2的元素值增加1
        System.out.println("增加后的值: " + atomicArray.get(2));

        // 原子地减少指定索引位置的元素值
        atomicArray.getAndDecrement(4); // 索引4的元素值减少1
        System.out.println("减少后的值: " + atomicArray.get(4));

        // 原子地增加指定索引位置的元素值,并返回增加前的值
        int previousValue = atomicArray.getAndAdd(1, 10); // 索引1的元素值增加10
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + atomicArray.get(1));
    }
}

引用类型

AtomicStampedReference类是Java中提供的一个用于解决ABA问题的原子类。它通过引入版本号(stamp)来追踪共享变量的变化过程,从而在CAS操作中比较变化过程是否符合预期

AtomicStampedReference类的构造方法如下:

/**
 * @param initialRef 初始引用
 * @param initialStamp 初始版本号
 */
public AtomicStampedReference(V initialRef, int initialStamp)

AtomicStampedReference类提供了以下主要方法:

  • boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp):尝试以原子方式将引用和版本号设置为新值,只有当当前引用和版本号与期望值相等时才会成功。
  • V getReference():获取当前的引用值。
  • int getStamp():获取当前的版本号。
  • V get(int[] stampHolder):获取当前的引用值,并将当前的版本号存储在stampHolder数组中的第一个元素中。
  • void set(V newReference, int newStamp):设置新的引用值和版本号。

使用 AtomicStampedReference 解决ABA问题的示例:

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceExample {
    private static AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        // 线程A先将值从A改为B,再改回A
        Thread threadA = new Thread(() -> {
            int stamp = reference.getStamp();
            String value = reference.getReference();

            reference.compareAndSet(value, "B", stamp, stamp + 1);
            reference.compareAndSet("B", "A", stamp + 1, stamp + 2);
        });

        // 线程B在A操作的过程中执行CAS操作
        Thread threadB = new Thread(() -> {
            int stamp = reference.getStamp();
            String value = reference.getReference();

            // 等待线程A完成第一次操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = reference.compareAndSet(value, "C", stamp, stamp + 1);
            System.out.println("CAS result: " + result);
        });

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();

        System.out.println("Final value: " + reference.getReference());
    }
}

在上述示例中,线程A先将共享变量的值从A改为B,再改回A。线程B在线程A的操作过程中执行CAS操作,尝试将共享变量的值从A改为C。由于AtomicStampedReference引入了版本号,CAS操作会比较当前的引用值和版本号是否与期望值相等,从而避免了ABA问题

对象的属性修改类型

原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

AtomicIntegerFieldUpdater类提供了一种线程安全的方式来对指定类的字段进行原子操作,无需使用synchronized关键字或volatile修饰符

注意:AtomicIntegerFieldUpdater仅适用于实例变量,不能用于静态变量

AtomicIntegerFieldUpdater 类使用示例:

public class AtomicIntegerFieldUpdaterExample {
    public static void main(String[] args) {
        // 定义一个包含一个int类型字段的类
        class MyClass {
            public volatile int myField;
        }

        // 创建AtomicIntegerFieldUpdater对象
        AtomicIntegerFieldUpdater<MyClass> updater = AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "myField");

        // 创建一个MyClass对象
        MyClass obj = new MyClass();

        // 原子地将字段值增加1,并返回增加前的值
        int previousValue = updater.getAndIncrement(obj);
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + updater.get(obj));

        // 原子地将字段值减少1,并返回减少前的值
        previousValue = updater.getAndDecrement(obj);
        System.out.println("之前的值: " + previousValue);
        System.out.println("减少后的值: " + updater.get(obj));

        // 原子地增加字段值,并返回增加前的值
        previousValue = updater.getAndAdd(obj, 10);
        System.out.println("之前的值: " + previousValue);
        System.out.println("增加后的值: " + updater.get(obj));
    }
}

参考资料:

  1. Java魔法类:Unsafe应用解析
  2. `JUC原子类: CAS, Unsafe和原子类详解
  3. 非阻塞同步算法与CAS无锁算法

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日一题 2487. 从链表中移除节点(中等,回溯)

显然只要从后往前遍历链表&#xff0c;设 t 为当前的最大值&#xff0c;只要在遍历过程中比 t 小的节点都删除&#xff0c;大于等于 t 的则更新 t 为新的节点 通过递归回溯的方法可以很简单地实现从后往前遍历链表 # Definition for singly-linked list. # class ListNode: # …

Qt第一个UI程序设计

在第一个Qt程序的基础上我对ui界面进行设计&#xff0c;点击设计按钮 然后 拖动Label按钮输入想要输入的语句。 运行结果如下图。

【LMM 008】Instruction Tuning with GPT-4

论文标题&#xff1a;Instruction Tuning with GPT-4 论文作者&#xff1a;Baolin Peng, Chunyuan Li, Pengcheng He, Michel Galley, Jianfeng Gao 作者单位&#xff1a;Microsoft Research 论文原文&#xff1a;https://arxiv.org/abs/2304.03277 论文出处&#xff1a;– 论文…

海外服务器2核2G/4G/8G和4核8G配置16M公网带宽优惠价格表

腾讯云海外服务器租用优惠价格表&#xff0c;2核2G10M带宽、2核4G12M、2核8G14M、4核8G16M配置可选&#xff0c;可以选择Linux操作系统或Linux系统&#xff0c;相比较Linux服务器价格要更优惠一些&#xff0c;腾讯云服务器网txyfwq.com分享腾讯云国外服务器租用配置报价&#x…

计算机组成原理 存储器概述,主存系统模型和RAM和ROM

文章目录 存储器概述基本概念存储器层次结构存储器分类性能指标 主存系统模型和结构存储元结构主存寻址 RAM和ROMRAM概念RAM对比DRAM刷新集中刷新分散刷新异步刷新 ROM 存储器概述 #mermaid-svg-EjCg9aMsdPUw7lra {font-family:"trebuchet ms",verdana,arial,sans-se…

Docker 安装Mysql

目录 Docker Mysql安装 ✨安装和配置mysql ✨远程连接mysql远程连接 MySQL 是世界上最流行的开源数据库。根据 DB-Engines的调查数据&#xff0c;MySQL 是第二受欢迎的数据库&#xff0c;仅次于 Oracle 数据库。MySQL在过去由于性能高、成本低、可靠性好&#xff0c;已经成…

计算机丢失mfc110.dll的5种常用解决方法分享

丢失动态链接库文件&#xff08;DLL&#xff09;是比较常见的一种情况&#xff0c;其中之一就是“计算机丢失mfc110.dll”。这个问题通常是由于系统文件损坏或缺失引起的&#xff0c;给计算机的正常运行带来了困扰。为了解决这个问题&#xff0c;我总结了以下五种方法&#xff…

项目进度管理:制定项目计划的要点

项目管理过程中不可避免项目实际进度和计划不一致的情况&#xff0c;无论是工作提前完成还是进度落后&#xff0c;一开始的进度计划都脱不了干系。 任务的明确性、计划的制定&#xff0c;皆有其规矩可循。一接到项目或任务便急匆匆地开始&#xff0c;边做边调整只会带来无尽的…

Acrel-EIoT能源物联网云平台助力电力物联网数据服务 ——安科瑞 顾烊宇

摘要&#xff1a;Acrel-EIOT能源物联网云平台是一个结合在线销售的互联网商业模式&#xff0c;为分布广泛的互联网用户提供PAAS服务的平台。安科瑞物联网产品安装完成后&#xff0c;用户可以通过手机扫描代码轻松实现产品访问平台&#xff0c;无需注意调试和平台运行过程&#…

内网离线搭建之----nginx配置ssl高可用

一、证书生成 1.生成服务端私钥 openssl genrsa -des3 -out server.key 2048 2.去除server.key密码 ps&#xff1a;否则每次用到都需要输入密码 openssl rsa -in server.key -out server.key 3.生成证书的签名 ps&#xff1a;使用机构颁发证书的到这一步就可以了&#xff…

【谷歌云】注册谷歌云 创建Compute Engine

文章目录 一、Google Cloud注册1.1 账号信息1.2 付款信息验证1.3 验证成功 二、Compute Engine创建2.1 启动Compute Engine API2.2 创建实例2.3 新建虚拟机实例2.4 等待实例创建完成2.5 查看虚拟机配置信息2.6 创建防火墙规则2.7 SSH远程连接虚拟机 三、参考链接 一、Google Cl…

c语言和python区别哪个难,c语言和python区别大不大

大家好&#xff0c;给大家分享一下c语言和python区别主要用来写什么&#xff0c;很多人还不知道这一点。下面详细解释一下。现在让我们来看看&#xff01; Python可以说是目前最火的语言之一了&#xff0c;人工智能的兴起让Python一夜之间变得家喻户晓&#xff0c;Python号称目…

基于双闭环PI和SVPWM的PMSM控制器simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 双闭环PI控制器设计 4.2 SVPWM技术 4.3 控制系统实现 5.完整工程文件 1.课题概述 基于双闭环PI和SVPWM的PMSM控制器simulink建模与仿真。系统包括逆变桥、PMSM、park变换、clark变换、SVPWM、PI控…

API集群负载统计 - 华为OD统一考试

OD统一考试 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 某个产品的RESTful API集合部署在服务器集群的多个节点上&#xff0c;近期对客户端访问日志进行了采集&#xff0c;需要统计各个API的访问频次&#xff0c;根据热点信息在服务器节点之间做负载…

快速打通 Vue 3(三):Vue3 中的 watch 监听器与新特性

很激动进入了 Vue 3 的学习&#xff0c;作为一个已经上线了三年多的框架&#xff0c;很多项目都开始使用 Vue 3 来编写了 这一组文章主要聚焦于 Vue 3 的新技术和新特性 如果想要学习基础的 Vue 语法可以看我专栏中的其他博客 Vue&#xff08;一&#xff09;&#xff1a;Vue 入…

原生JS做别踩白块游戏

思路 创建初始一个按钮并为他添加点击监听开始创建随机方块&#xff0c;并样式_box.offsetTop speed px结合setInterval使得方块不断下移创建和删除方块的原则&#xff1a;box.offsetTop>0&#xff08;可视区上部没有方块了&#xff09;时候需要创建一行方块&#xff0c;…

【HarmonyOS开发】通过媒体查询,实现一次开发,多端部署

媒体查询&#xff08;Media Queries&#xff09;是一种在CSS中使用的技术&#xff0c;用于根据设备的特性和属性&#xff08;如屏幕宽度、设备类型等&#xff09;来应用不同的样式规则。通过媒体查询&#xff0c;可以根据不同的设备或屏幕尺寸为用户提供优化的布局和样式。 1、…

three.js: gltf模型设置发光描边

效果&#xff1a; 代码 &#xff1a; <template><div><el-container><el-main><div class"box-card-left"><div id"threejs" style"border: 1px solid red"></div><div style"padding: 10px…

C++Qt6 哈夫曼编码求解 数据结构课程设计 | JorbanS

一、 问题描述 在进行程序设计时&#xff0c;通常给每一个字符标记一个单独的代码来表示一组字符&#xff0c;即编码。在进行二进制编码时&#xff0c;假设所有的代码都等长&#xff0c;那么表示 n 个不同的字符需要 位&#xff0c;称为等长编码。如果每个字符的使用频率相等&…
最新文章