【Java难点】多线程-终极【更新中...】

Java内存模型之JMM

为什么需要JMM

计算机存储结构:从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算。

image-20240508225728103

CPU和物理主内存的速度不一致,所以设置多级缓存,CPU运行时并不会直接操作内存,当CPU读取数据时,先把内存里边的数据读到缓存,然后再从缓存中读取;当CPU写出数据时,先把数据写到缓存中,然后缓存再写到内存中。

JVM规范中定义了一种Java内存模型 (java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

image-20240508231033593

JMM的作用:

  1. 通过JMM来实现线程和主内存之间的抽象关系。

  2. 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

什么是JMM

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对共享变量的写入何时可用,以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性可见性有序性展开的。

JMM规范下的三大特性
可见性

可见性是一种即时通知机制,当一个线程修改了某一个共享变量的值,其他线程能够立即知道该变更

JMM规定了所有的变量都存储在主内存中。

image-20240508232744054

系统主内存共享变量数据修改时被写入的时间是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存(线程私有),线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

image-20240508233358671

线程读取变量过程:

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取賦值等)必须在工作内存中进行。首先要将变量从主内存拷贝到线程自己的工作内存空问,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

image-20240509204132072

线程和主内存之间的关系:

  1. 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
  2. 每个线程都有一个私有的本地工作内存,木地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)

线程脏读:

image-20240508233904199

原子性

指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰

有序性

重排序:

对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。但是处理器在进行重排序时必须要考虑指令之间的数据依赖性

重排序的优缺点:

优:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能

缺: 但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生"脏读")。简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

从源码到最终执行示例图:

image-20240508234923953

单线程环境里面可以保证程序最终执行结果与顺序执行的结果一致。但是,在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

happens-before

Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A发生过的事情对B来说是可见的,无论 A事件和B事件是否发生在同一个线程里。

JMM的设计分为两部分:

一部分是面向我们程序员提供的,也就是happens-before规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了。

另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。

我们写代码时,只需要关注前者就好了,也就是理解happens before规则即可,其它繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写好代码即可。

8条规则:

  1. 次序规则:在同一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。
  2. 锁定规则: 一个unlock操作先行发生于后面(这里的“后面”是指时间上的先后)对同一个锁的lock操作。

image-20240509210053471

  1. volatitle变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。

  2. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

  3. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

  4. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断,也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发送。

  5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否己经终止执行。

  6. 对象终结规则:一个对象的初始化完成(构造函数执行结東)先行发生于它的finalize()方法的开始。

案例:

image-20240509213004706

问:假设存在线程A和B,线程A先(时间上的先后)调用了setValue(),然后线程B调用了同一个对象的getValue),那么线程B收到的返回值是什么?

答:不一定

原因:

我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8可以忽略,因为他们和这段代码毫无关系):

  1. 由于两个方法是由不同的线程调用,不在同一个线程中,所以肯定不满足规则1(次序规则);
  2. 两个方法都没有使用锁,所以不满足规则2(锁定规则);
  3. 变量不是用volatile修饰的,所以不满足规则3(volatile变量规则)
  4. 规则4(传递规则)肯定不满足;

所以我们无法通过happens-before 原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B执行,但就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。

那么怎么修复这段代码呢?

修复1:把getter/setter方法都定义为synchronized方法

image-20240509214228484

修复2:把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字使用场景

image-20240509214521121

volatile与JMM

**volatile变量的2大特点:**可见性、有序性

  1. 可见性

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

当读一个volavle变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。

所以volatile的写内存语义是写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面所有线程可见。

  1. 有序性

禁止编译器指令重排

volatile凭什么可以保证可见性和有序性???

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存,

内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

image-20240509223835161

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

内存屏障粗分为2种:

读屏障:在读指令之前插入读屏障,让工作内存或CPU高速级存当中的缓存数据失效,重新回到主内存中获取最新数据

写屏障:在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中

内存屏障细分为4种:

image-20240509225117536

happens-before之volatile变量规则:

image-20240509225745692

image-20240509230430344

image-20240509230402081

volatile之可见性案例
  • 不使用volatile
import java.util.concurrent.TimeUnit;

public class JUC08 {
    static boolean flag=true;
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t -----come in");
            while(flag){}
            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
        },"t1").start();

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

        flag=false;
        System.out.println(Thread.currentThread().getName()+"\t -----修改完成:"+flag);
    }
}

image-20240509231747572

线程t1中为何看不到被主线程main修改为false的flag的值?

  1. 可能主线程修改了flag之后没有将其刷新到主内存,所以1线程看不到。

  2. 可能主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。

  • 使用volatile
import java.util.concurrent.TimeUnit;

public class JUC08 {
    static volatile boolean flag=true;
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t -----come in");
            while(flag){}
            System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
        },"t1").start();

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

        flag=false;
        System.out.println(Thread.currentThread().getName()+"\t -----修改完成:"+flag);
    }
}

image-20240509231846119

volatile之原子性案例
  • volatile不能保证原子性
import java.util.concurrent.TimeUnit;

public class JUC08 {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(myNumber.number);
    }
}
class MyNumber{
    volatile int number;
    public void addPlusPlus(){
        number++;
    }
}

image-20240510002354034

  • 使用synchronized保证原子性
import java.util.concurrent.TimeUnit;

public class JUC08 {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myNumber.addPlusPlus();
                }
            },String.valueOf(i)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(myNumber.number);
    }
}
class MyNumber{
    int number;
    public synchronized void addPlusPlus(){
        number++;
    }
}

image-20240510002544423

volatile不能保证原子性的原因:

假设A线程中i=1时,读取到number为5,B线程i=1时,也读取到number为5,A线程对number进行++,A线程的工作内存中number=6,此时B线程刚好也执行完number++,此时B线程的工作内存中number=6,假设B线程正要将number=6写入主内存时,A线程先一步将number=6写入主内存,此时主内存中number=6,B线程感知到主内存中number发生了改变,B线程将会把自己工作内存中的number=6丢掉,下一次读取主内存中的number时会将从主内存中读取最新的值,B线程执行myNumber.addPlusPlus();下面的代码,而myNumber.addPlusPlus();下面没有代码,所以B线程进入下一次循环,此时B线程i=2,从主内存中读取最新的number=6,但是B线程i=1时的并没有有效的将number++,所以最终的number一定会小于10000。

image-20240510000141383

详见《深入理解Java虚拟机》12.3.3节

注意:

  1. volatile变量不适合参与到依赖当前值的运算中,如i=i+1;i++之类的,volatile通常用做保存某个状态的boolean值或int值。
  2. 由于volatile变量只能保证可见性,所以我们仍然要通过加锁(使用synchronized、 java.util.concurrent中的锁或原子类)来保证原子性。
volatile之禁重排案例

若存在数据依赖关系则禁止重排序===>重排序发生,会导致程序运行结果不同。

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序 两个操作的执行顺序,程序的执行结果就会被改变

image-20240510205725732

案例

image-20240510211740759

image-20240510211538446

volatile使用场景
  • 作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束

image-20240510212529394

  • 当读远多于写,结合使用內部锁和volatile 变量来减少同步的开销

image-20240510212501101

  • DCL双端锁的发布

image-20240510213203961

总结

添加volatile关键字后,JVM为什么会加入内存屏障?

image-20240510213708543

CAS

CAS原理

定义:

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

执行CAS操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

CAS时一种系统原语,原语属于操作系统用语范畴,是由若干指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

原子类:

java.util.concurrent.atomic包下的所有类,原子类是CAS思想的落地。

实例:

CAS有3个操作数:V、A、B,其中V:要修改属性所在的内存地址,A:旧的预期值,B:修改后的新值。当且仅当旧的A和V对应的属性值相同时,才将V对应的属性值修改为B,否则什么都不做或重试。它重试的这种行为称为----自旋!!

CAS硬件级别的保证原子性:

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,
比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好

案例:

import java.util.concurrent.atomic.AtomicInteger;

public class JUC08 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2022)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2023)+"\t"+atomicInteger.get());
    }
}

image-20240510223541883

源码:

image-20240510224346539

compareAndSet()方法的源代码:

image-20240510224241652

UnSafe类

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

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

image-20240510225516902

变量valueOfset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

AtomicInteger的incrementAndGet方法:

image-20240510230923094

image-20240510231111339

实例

image-20240510231804975

原子引用(AtomicReference)
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.concurrent.atomic.AtomicReference;

public class JUC08 {
    public static void main(String[] args) {
        User z3 = new User("z3", 22);
        User li4 = new User("li4", 28);
        User w5 = new User("w5", 33);
        AtomicReference<User> userAtomicReference = new AtomicReference<>(z3);
        System.out.println(userAtomicReference.compareAndSet(z3, li4)+"\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(z3, w5)+"\t"+userAtomicReference.get().toString());
    }
}
@Data
@AllArgsConstructor
class User{
    String name;
    Integer age;
}

image-20240510233053535

自旋锁

CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋锁的实现:

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

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class JUC08 {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t-------come in");
        while (!atomicReference.compareAndSet(null, thread)) {} //自旋等待
    }
    public void unlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t-------task over,unLock...");
    }

    public static void main(String[] args) {
        JUC08 juc08 = new JUC08();
        new Thread(()->{
            juc08.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            juc08.unlock();
        },"A").start();

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

        new Thread(()->{
            juc08.lock();
            juc08.unlock();
        },"B").start();
    }
}

image-20240510235251142

CAS缺点
  • 循环时间长开销很大

image-20240511002022278

  • ABA问题

image-20240511002617608

解决ABA问题: 版本号时间戳原子引用(AtomicStampedReference)

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.concurrent.atomic.AtomicStampedReference;

public class JUC08 {
    public static void main(String[] args) {
        Book javaBook = new Book(1,"javaBook");
        Book mysqlBook = new Book(2,"mysqlBook");
        //第二个参数为初始流水号
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook, 1);
        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
        System.out.println(stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp()+1));
        System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
    }
}
@Data
@AllArgsConstructor
class Book{
    private Integer id;
    private String bookName;
}

image-20240511004351983

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

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

相关文章

SinoDB数据库出现长事务的解决方法

SinoDB数据库出现长事务的具体现象&#xff1a;   长事务会引发逻辑日志耗尽&#xff0c;导致数据库进入叫做“长事务阻塞Blocked:LONGTX”的状态中&#xff0c;数据库服务响应停止。这时候&#xff0c;数据库状态通过onstat – 命令通常有如下提示&#xff1a; Sinoregal Si…

DSP ARM FPGA 实验箱_音频处理_滤波操作教程:3-9 音频信号的滤波实验

一、实验目的 掌握Matlab辅助设计滤波器系数的方法&#xff0c;并实现音频混噪及IIR滤波器滤除&#xff0c;并在LCD上显示音频信号的FFT计算结果。 二、实验原理 音频接口采用的是24.576MHz&#xff08;读兆赫兹&#xff09;晶振&#xff0c;实验板上共有3个音频端口&#x…

计算机视觉:三维重建技术

书籍&#xff1a;Computer Vision&#xff1a;Three-dimensional Reconstruction Techniques 作者&#xff1a;Andrea Fusiello 出版&#xff1a;Springer 书籍下载-《计算机视觉&#xff1a;三维重建技术》​本书探讨了用于通过图像确定实体物体的几何属性的理论和计算技术…

将来会是Python、Java、Golang三足鼎立吗?

在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「 Java的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 软件工程里没有银弹&#xff…

Java方法的重载

方法重载 1. 为什么需要方法重载 public class TestMethod{public static void main (String[] args){int a 10;int b 20;int ret add(a,b);System.out.println("ret "ret);double a2 10.5;double b2 20.5;double ret2 add(a2,b2);System.out.println("…

layui select 绑定onchange事件失效

layui select 绑定onchange事件失效 问题背景解决方案 问题背景 在日常工作中&#xff0c;可能会用到页面 freemaker 以及 layui 前端框架&#xff0c;这个时候直接在 select 上面绑定 onchange 事件往往是不生效的&#xff0c;错误的方式 这种方式给 select 绑定的 onchange…

OpenCV学习(一)银行卡号识别

B站教学链接&#xff1a; 银行卡号识别&#xff08;OpenCV-Python&#xff09; 代码&#xff1a;Card_Number_Recognition 1.方法 使用模板匹配的方法来识别银行卡号数字。具体来说&#xff0c;先通过图像预处理将图像中的银行卡号分割出来&#xff0c;再与提供的模板进行…

elementui的table行展开,左侧的icon有的需要有的不需要

百度了一些方法&#xff0c;都不好用&#xff0c;最后还是纯css解决&#xff0c;以下是效果&#xff1a; 代码实现&#xff1a; :deep(.el-table__row:nth-child(1) .el-table__expand-icon){ display: none; }

node pnpm修改默认包的存储路径

pnpm与npm的区别 PNPM和NPM是两个不同的包管理工具。 NPM&#xff08;Node Package Manager&#xff09;是Node.js的官方包管理工具&#xff0c;用于安装、发布和管理Node.js模块。NPM将包安装在项目的node_modules目录中&#xff0c;每个包都有自己的依赖树。 PNPM&#xf…

如何使用 ERNIE 千帆大模型基于 Flask 搭建智能英语能力评测对话网页机器人(详细教程)

ERNIE 千帆大模型 ERNIE-3.5是一款基于深度学习技术构建的高效语言模型&#xff0c;其强大的综合能力使其在中文应用方面表现出色。相较于其他模型&#xff0c;如微软的ChatGPT&#xff0c;ERNIE-3.5不仅综合能力更强&#xff0c;而且在训练与推理效率上也更高。这使得ERNIE-3…

ESP32引脚入门指南(五):从理论到实践(SPI)

ESP32 微控制器因其丰富的外设接口而备受赞誉&#xff0c;其中SPI&#xff08;Serial Peripheral Interface&#xff09;是一种常见的通信协议。本文将深入探讨ESP32的SPI、HSPI&#xff08;High-Speed SPI&#xff09;和VSPI&#xff08;Very High-Speed SPI&#xff09;接口&…

3DGS+3D Tiles融合已成 ,更大的场景,更细腻的效果~

最近国外同行Kieran Farr发布了一个他制作的3D GussianSplatting(高斯泼溅)Google Map 3D Tiles的融合叠加的demo案例&#xff08;如下所示&#xff09;。 准确来说这是一个数据融合的实景场景&#xff0c;该实景场景使用了倾斜三维和3D GussianSplatting两种实景表达技术&…

CSS跳动文字

<div class"loading-mask"><div class"loading-text"><span style"--i:1">加</span><span style"--i:2">载</span><span style"--i:3">中</span><span style"--i:…

JCR一区 | Matlab实现1D-2D-GASF-CNN-GRU-MATT的多通道输入数据分类预测

JCR一区 | Matlab实现1D-2D-GASF-CNN-GRU-MATT的多通道输入数据分类预测 目录 JCR一区 | Matlab实现1D-2D-GASF-CNN-GRU-MATT的多通道输入数据分类预测分类效果基本介绍程序设计参考资料 分类效果 基本介绍 基本介绍 Matlab实现1D-2D-GASF-CNN-GRU-MATT的多通道输入数据分类预…

uni-app(五):原生插件打包并使用(Android)

原生插件打包并使用 解决Gradle不显示命令问题解决方法 运行打包查看打包好的包引入到uni-app项目中编写配置文件TestModuleTestComponent 制作基座并运行 解决Gradle不显示命令问题 解决方法 运行打包 查看打包好的包 引入到uni-app项目中 编写配置文件 TestModule {"n…

【操作系统期末速成】​操作系统概述(定义|功能|特征)|发展阶段和分类|结构设计|概念补充

&#x1f3a5; 个人主页&#xff1a;深鱼~&#x1f525;收录专栏&#xff1a;操作系统&#x1f304;欢迎 &#x1f44d;点赞✍评论⭐收藏 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到…

使用 MSYS2 Qt6 发布绿色版的SDR软件无线电应用

文章目录 概要整体架构流程技术名词解释技术细节在启动器中为子进程设置路径和环境。如何迅速找齐所有的DLL 小结附件 概要 新接触软件定义无线电&#xff08;SDR&#xff09;的朋友一般都会一股脑的安装一些现有的SDR平台。无论是GNURadio还是SDR、SDRSharp、SDRAngel&#x…

数字音频的采样和量化

一.PCM&#xff08;Pulse-Code Modulation 脉冲编码调制&#xff09; PCM是一个无损无压缩的&#xff08;相较于有损压缩&#xff0c;如果相对于模拟信号是有损的&#xff09;数字化编码方式&#xff08;PCM不单单应用于音频领域&#xff0c;本文只介绍在音频领域中的应用&…

R2S+ZeroTier+Trilium

软路由使用ZeroTier搭建远程笔记 软路由使用ZeroTier搭建远程笔记 环境部署 安装ZeroTier安装trilium 环境 软路由硬件&#xff1a;友善 Nanopo R2S软路由系统&#xff1a;OpenWrt&#xff0c;使用第三方固件nanopi-openwrt。内网穿透&#xff1a;ZeroTier。远程笔记&…

Arduino-ILI9341驱动介绍二

Arduino-ILI9341驱动介绍二 1.概述 第一篇文章介绍了Arduino-点亮TFT触摸屏&#xff0c;没有介绍如何改变屏幕的内容。这篇文章介绍Arduino-使用ILI9341驱动控制TFT触摸屏原理和ILI9341驱动源代码设计原理以及常用函数 2.Arduino控制TFT触控屏原理 Arduino使用什么方式控制…