【多线程】(二)线程安全问题与线程同步

news2024/11/28 10:52:32

文章目录

  • 一、多线程带来的风险
    • 1.1 观察线程不安全
    • 1.2 线程安全概念
    • 1.3 线程不安全的原因
    • 1.4 线程安全的解决方法
  • 二、synchronized关键字
    • 2.1 synchronized 的特性
    • 2.2 synchronized 使用示例
    • 2.3 Java 标准库中的线程安全类
  • 三、volatile关键字
    • 3.1 保证内存可见性
    • 3.2 禁止指令重排序
    • 3.2 不保证原子性
  • 四、wait和notify方法
    • 4.1 wait方法
    • 4.2 notify方法
    • 4.3 wait 和 sleep 的对比


一、多线程带来的风险

1.1 观察线程不安全

首先来看一段代码,这段代码的功能是创建两个线程,分别对同一个变量各自增 5w 次,预期的结果应该是 10w。

class Counter{
    public int cnt = 0;
}

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.cnt++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.cnt++;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.cnt);
    }
}

多运行几次这段代码,其结果如下:

71794 // 第一次
66446 // 第二次
66572 // 第三次
59366 // 第四次

发现,这几次运行的结果都小于 10 w,原因就是这段代码是多线程运行的,存在线程安全问题。因为多个线程同时对counter.cnt进行写操作的时候,就会发生竞争。具体来说,当线程t1t2同时执行counter.cnt++时,可能发生以下情况:

  1. 假如t1读取counter.cnt的值为10,然后增加到11。
  2. 由于t1还没有将11写回内存的,因此t2读取counter.cnt的值也为10,然后增加到11。
  3. t1将11写回counter.cnt
  4. t2将11写回counter.cnt

当发生这种情况的时候,就相当于少加了一次,因此当各自循环 5w 次的时候,就会少加很多次,所以最后的结果就会比预期的少很多了。

1.2 线程安全概念

简单来说,线程安全就是在多线程环境下,对共享资源的访问和操作能够正确、可靠地进行,不会产生不确定的或者与预期不符合的结果。

在多线程编程中,多个线程同时访问和修改共享数据时,可能会引发以下问题:

  1. 竞态条件(Race Condition):多个线程竞争执行同一段代码,导致执行结果依赖于线程执行的时序,而非代码本身的逻辑。这可能导致不可预期的结果。

  2. 数据竞争(Data Race):多个线程同时读写共享数据,至少有一个线程进行写操作,而且没有适当的同步机制来保护共享数据。这可能导致数据的不一致性或破坏数据完整性。

  3. 死锁(Deadlock):两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行。

1.3 线程不安全的原因

1. 修改共享数据

例如在上述线程不安全的例子中,就涉及到两个线程对counter,cnt变量进行修改,此时的counter.cnt就是一个多个线程就能访问和修改的共享数据

  • 在多线程环境中,线程间共享的区域有堆区、方法区以及常量区。而上面的counter对象就存在于堆区,因此由多个线程所共享。
  • 当多个线程同时对共享数据进行读取和修改时,如果没有适当的同步机制保护共享数据,就会导致数据的不一致性。
  • 例如,在一个线程对共享数据进行修改时,另一个线程可能同时读取该数据,导致读取到了不一致的结果。

2. 原子性

什么是原子性:

  • 原子性就是指一个操作在执行过程中不可被中断,要么全部执行成功,要么全部不执行,不会出现执行一部分的情况。
  • 在多线程环境中,原子性是保证多个线程对共享数据进行操作时的一种特性。当一个操作具有原子性时,意味着它在执行过程中不会被其他线程干扰,保证了操作的完整性和一致性。
  • 原子性的特性可以通过不可分割性和独占性来概括。

在Java程序中,一条语句不一定是原子的,同时一条语句也不一定是一条指令,例如上面的counter.cnt++操作,其实是由三步操作组成:

  1. load:将内存中的cnt的值加载到CPU的寄存器当中;
  2. add:对CPU寄存器中的值进行自增操作;
  3. save:将CPU寄存器中运算的结果保存回内存。

因此,如果要保证计算结果的正确性,就必须要保证这三步操作是原子操作,如果不保证其是原子操作就会引发线程安全问题,比如:如果一个线程正在对一个变量操作,中途其他线程插队进来了,将这个操作打断了,最终结果就可能是错误的。

3. 可见性

可见性是指当一个线程对共享数据进行修改后,其他线程能够立即看到最新的修改结果。如果没有适当的同步机制,修改线程对数据的更新可能对其他线程是不可见的,导致其他线程继续使用过期的数据

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型。

  • 线程之间的共享变量存放在 主内存(Main Memory) 中。
  • 每一个线程都有自己的 工作内存(Working Memory)
  • 当线程要读取一个共享变量的时候,会先把变量从主内存中拷贝到自己的工作内存,再从工作内存中读取数据
  • 当线程要修改共享变量的时候,就会先修改自己工作内存中的副本,然后在同步回主内存

由于每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的 “副本”。此时修改 线程1 的工作内存中的值,线程2 的工作内存不一定会及时变化。例如下面的情况:

1)初始情况下,两个线程的工作内存内容一致:

2)一旦 线程1 修改了 a 的值,此时主内存不一定能及时同步。对应的 线程2 的工作内存的 a 的值也不一定能及时同步:

3)如果 线程2 的值还没有同步的时候,在对 线程2 中的值进行修改操作,就会导致最终的结果与预期不符,也就引发的线程安全问题。

4. 代码顺序性

什么是代码重排序:
代码重排序是指在编译器或处理器优化执行的过程中,改变原始程序中语句的执行顺序,以提高性能或满足其他需求。代码重排序可以分为编译器重排序和处理器重排序两种类型。

比如,在执行构造函数的时候,需要执行的顺序如下:

  1. 在内存中为对象开辟一块空间;
  2. 为对象进行初始化操作;
  3. 返回对象的引用给变量。
  • 如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1 -> 3 -> 2 的方式执行,我们发现也是没有问题的。这种就叫做指令重排序。

  • 但是如果在多线程的环境中,比如是懒汉模式的单例模式,即单利对象在第一次被调用的时候初始化。此时,线程1 发现单例对象没有被创建,就按照 1 -> 3 -> 2 的顺序创建这个对象,此时当执行完步骤3的时候,因为某些原因都在被切换走了。

  • 此时 线程2 也要使用单例对象,发现该单例对象存在了(因为线程一中返回了该单例对象的引用),但是没有初始化,所有就会引发内存错误。

因此在有些多线程情况下,就需要禁止重排序问题。

1.4 线程安全的解决方法

实现线程安全的解决方法可以包括以下几种常见的技术和策略:

  1. 使用锁机制:使用互斥锁(Mutex Lock)或重入锁(Reentrant Lock)来保证同一时间只有一个线程可以访问共享资源。通过在关键代码块或方法上添加锁来确保互斥访问,防止多个线程同时修改共享数据。

  2. 使用原子操作:使用原子类(Atomic Classes)或volatile关键字来保证特定操作的原子性。原子操作是不可分割的,可以保证在多线程环境下对共享数据的修改是线程安全的。

  3. 使用线程安全的数据结构:使用线程安全的容器和数据结构,如ConcurrentHashMapCopyOnWriteArrayList等,它们内部实现了线程安全的操作机制,可以直接在多线程环境中使用。

  4. 使用并发工具类:使用并发工具类来协调和控制多个线程的执行,例如SemaphoreCountDownLatchCyclicBarrier等,它们提供了灵活的线程同步和线程间通信的方式。

  5. 使用不可变对象:设计不可变对象,确保对象的状态在创建后不能被修改,从而避免多线程环境下的并发问题。不可变对象不需要额外的同步机制,因为它们的状态是固定的,不会被多线程同时修改。

  6. 同步代码块或方法:使用synchronized关键字来对关键代码块或方法进行同步,确保同一时间只能有一个线程执行该代码块或方法,从而保证线程安全。

二、synchronized关键字

synchronized 是 Java 中用于实现线程同步的关键字。它可以用于方法或代码块上,用于保证在同一时间只有一个线程可以访问被标记为 synchronized 的代码。

2.1 synchronized 的特性

1)互斥
synchronized 关键字具有互斥的特性,确保在同一时间只有一个线程可以获取到锁,并执行被保护的代码块或方法。没有获取到锁的其他线程需要阻塞等待,直到持有锁的线程释放锁。这样可以避免多个线程同时访问共享资源而引发的数据竞争问题。

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁


这里,synchronized用的锁是counter对象。

2)内存刷新

synchronized 的工作过程大致可以总结为如下情况:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

当一个线程进入或退出一个 synchronized 块时,它会自动将对共享变量的修改刷新到主内存中,以保证共享变量的可见性。当其他线程获取锁时,它们会从主内存中读取最新的共享变量值,而不是使用线程自己的缓存值。这样可以确保线程之间对共享变量的读写操作具有一致的视图。

3)可重入

可重入锁:

  • 可重入锁(Reentrant Lock)是一种支持重入特性的锁机制。它允许线程在持有锁的情况下再次获取同一个锁,而不会被自己所拥有的锁所阻塞。
  • 可重入锁解决了在递归调用或嵌套代码中对同一个锁的重复获取的问题。当一个线程已经获得了可重入锁的锁对象时,它可以继续多次获取该锁,而不会因为自身已经持有锁而被阻塞。
  • 这样可以确保线程对共享资源的访问是安全的,同时提供了更高的灵活性和方便性。

synchronized 关键字支持可重入,即一个线程在已经持有锁的情况下,可以再次获取同一个锁而不会被自己所拥有的锁所阻塞。这种机制使得线程可以在同一个递归方法或代码块中进行嵌套调用,而不会出现死锁或阻塞的情况。

比如下面这段代码,重复加锁也不会造成阻塞,这就是锁的可重入。

synchronized (counter) {
    synchronized (counter) {
        counter.cnt++;
    }
}

可重入锁的实现原理:

在可重入锁的内部,包含了 “线程持有者” 和 “计数器” 两个信息。

  • 如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增。
  • 解锁的时候计数器递减为 0 的时候,才真正释放锁,此时锁才能被别的线程获取到。

2.2 synchronized 使用示例

例如文章开头哪个例子,此时我们为两个线程的counter.cnt++操作都加上synchronized关键字:

Thread t1 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
        synchronized (counter) {
            counter.cnt++;
        }
    }
});

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
        synchronized (counter) {
            counter.cnt++;
        }
    }
});

此时无论运行多少次,其结果都是 10 w 了。

2.3 Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施,比如:

ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

因此,在多线程中使用这些容器的时候就可能需要我们自己进行加锁,或者换用线程安全的容器类。

锁机制来实现线程安全的容器类有:

Vector
HashTable
ConcurrentHashMap
StringBuffer

其中,StringBuffer 的核心方法都带有 synchronized ,所有是线程安全的,另外String类也是线程安全的,虽然没有涉及到加锁操作,但是String类不涉及到修改,所有是线程安全的。

三、volatile关键字

volatile 是 Java 中的关键字,用于声明变量,表示该变量是易变的(volatile variable)。使用 volatile 关键字修饰的变量具有以下特性:

  1. 可见性(Visibility)

  2. 禁止指令重排序(Prevents Instruction Reordering)

在多线程编程中,volatile 关键字可以用于确保对共享变量的安全访问和可见性,但它并不能解决所有的线程安全问题。例如,它不能保证原子性操作和复合操作的线程安全性。对于这些情况,需要使用其他同步机制,如锁(synchronized)或原子类(Atomic classes)。

3.1 保证内存可见性

volatile 保证了变量的修改对所有线程可见。当一个线程修改了一个 volatile 变量的值时,这个修改将立即被写入主内存,并且其他线程在读取该变量时将从主内存中获取最新值,而不是使用线程的本地缓存值。这样可以确保所有线程对于该变量的读写操作具有一致的视图。

代码在写入 volatile 修饰的变量的时候:

  • 改变线程工作内存中volatile变量副本的值;
  • 将改变后的副本的值从工作内存刷新到主内存。

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中;
  • 从工作内存中读取volatile变量的副本。

例如以下代码,展示了volatile关键字的重要性:

在这个代码中

  1. 创建两个线程 t1 和 t2;
  2. t1 中包含一个循环,这个循环以 flag == 0 为循环条件;
  3. t2 中从键盘读入一个整数,并把这个整数赋值给 flag;
  4. 预期当用户输入非 0 的值的时候,t1 线程结束;
static class Flag {
    public int flag = 0;
}

public static void main(String[] args) {
    Flag flag = new Flag();
    Thread t1 = new Thread(() -> {
        while (flag.flag == 0) {
            // do nothing
        }
        System.out.println("循环结束!");
    });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        flag.flag = scanner.nextInt();
    });
    t1.start();
    t2.start();
}

运行这段代码:

输入一个非 0 的值,发现程序并没有终止。因为 t1 读的是自己工作内存中的内容,当 t2 对 flag 变量进行修改,此时 t1 感知不到 flag 的变化。所有 t1 就会一直循环下去了。

如果给 flag 加上 volatile关键字:

volatile static class Flag {
    public int flag = 0;
}

此时,再次运行,就发现能够正常退出了。

3.2 禁止指令重排序

volatile 关键字禁止编译器和处理器对 volatile 变量相关的指令进行重排序。即使在编译器优化或处理器优化的过程中,保证 volatile 变量的读写操作按照代码的顺序执行。

3.2 不保证原子性

尽管 volatile 可以确保变量的可见性和禁止指令重排序,但它并不能保证复合操作的原子性。

如果多个线程同时对一个 volatile 变量进行读取和写入操作,每个线程的操作都是原子的,但是复合操作仍然可能存在竞态条件和数据竞争的问题。例如,对 volatile 变量的自增操作 count++ 并不是一个原子操作,它包括读取变量值、增加值、写回变量的三个步骤,因此在多线程环境中可能导致不一致的结果。

比如,在最开始的代码中,不加锁,只给cnt变量加上volatile关键字:


class Counter {
    volatile public int cnt = 0;
}

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.cnt++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.cnt++;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.cnt);
    }
}

此时可以看到,最终 cnt 的值仍然无法保证是 100000。

四、wait和notify方法

由于线程之间的执行是抢占式的,因此多个线程的执行顺序是不确定的。但是在实际开发过程中,往往都需要合理地协调多个线程,控制它们之间的执行顺序。

完成这些线程的协调工作就需要使用到 waitnotify方法:

  • wait:让当前线程进入等待状态。
  • notify:唤醒当前对象上等待的线程。

注意,waitnotify 都是 Object 类中的方法。

4.1 wait方法

在Java中,有三个wait方法:

方法说明
wait()使当前线程进入等待状态,直到被其他线程唤醒。
wait(long timeout)使当前线程进入等待状态,直到被其他线程唤醒或等待超时。
wait(long timeout, int nanos)使当前线程进入等待状态,直到被其他线程唤醒、等待超时或指定的纳秒数。

wait 做的事情:

  • 使当前执行代码的线程进行等待,即把这个线程放到等待队列中;
  • 释放当前的锁;
  • 满足一定条件时被唤醒,重新尝试获取这个锁。

注意,wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常。因为wait做的一件事之一就是要释放锁,如果没有锁怎么能释放呢。

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法;
  • wait 等待时间超时 (使用带有 timeout 参数的wait版本,来指定等待时间);
  • 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常。

代码示例:观察wait()方法使用

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
    }
}

执行这段代码,就会发现结果是一直处于“等待中”,因为没有线程去调用notify去唤醒它。

4.2 notify方法

在Java中,有两个notify方法:

方法说明
notify()随机选择其中一个等待线程进行唤醒。
notifyAll()唤醒在当前对象上等待的所有线程。

notify 方法是唤醒等待的线程:

  • 方法notify也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。
  • notify方法调用后,当前线程不会马上释放该对象锁,要等到执行notify`方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

代码示例: 使用notify()方法唤醒线程

  • 创建线程 t1,调用wait方法;
  • 创建线程 t2,调用 notify方法唤醒 t1 线程;
  • 注意,需要先让 t1 线程执行了wait方法,才让 t2 线程执行 notify 方法,因为如果notify先执行,那么后面的wait方法就无法唤醒了。
    public static void main(String[] args) throws InterruptedException {

        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            System.out.println("等待中");
            try {
                synchronized (locker) {
                    locker.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("等待结束");
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker){
                locker.notify();
            }
        });

        t1.start();
        Thread.sleep(1000);
        t2.start();
    }

4.3 wait 和 sleep 的对比

waitsleep 的区别可以总结如下:

  • wait() 方法和 sleep() 方法都可以使线程暂停执行,但它们的使用场景和效果不同。
  • wait() 方法用于线程间的协作和通信,一般与 notify()notifyAll() 方法一起使用,用于线程的等待和唤醒。
  • sleep() 方法用于线程的暂时休眠,不涉及线程间的通信。调用 sleep() 方法会暂停当前线程的执行,并让出 CPU 时间给其他线程。
  • wait() 方法会释放当前对象的锁,而 sleep() 方法不会释放锁。
  • wait() 方法必须在同步代码块或同步方法中调用,而 sleep() 方法可以在任何地方调用。
  • wait() 方法需要被其他线程通过 notify()notifyAll() 方法唤醒,而 sleep() 方法在指定的时间后会自动恢复执行。
  • wait() 方法可以指定等待的时间,而 sleep() 方法必须指定休眠的时间。
  • 在异常处理上,wait() 方法需要捕获并处理 InterruptedException 异常,而 sleep() 方法不需要捕获此异常(因为 sleep() 方法本身就会抛出 InterruptedException 异常)。

综上所述,wait() 方法和 sleep() 方法在使用方式和效果上有所区别,wait() 方法主要用于线程间的协作和通信,而 sleep() 方法主要用于线程的暂时休眠。

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

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

相关文章

Java反射的应用:动态代理

代理设计模式的原理 使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。 对于静态代理&#xff0c;特征是代理类和目标对象的类都是在编译期间确定下来&#xff0c;不利于程…

基于FPGA的按键消抖

文章目录 基于FPGA的按键消抖一、按键消抖原理二、按键消抖代码三、仿真代码编写四&#xff1a;总结 基于FPGA的按键消抖 一、按键消抖原理 按键抖动&#xff1a;按键抖动通常的按键所用开关为机械弹性开关&#xff0c;当机械触点断开、闭合时&#xff0c;由于机械触点的弹性…

怎么使用Netty解码自定义通信协议

网络协议的基本要素 一个完备的网络协议需要具备哪些基本要素 魔数&#xff1a;魔数是通信双方协商的一个暗号&#xff0c;通常采用固定的几个字节表示。魔数的作用是防止任何人随便向服务器的端口上发送数据。协议版本号&#xff1a;随着业务需求的变化&#xff0c;协议可能…

SAP顾问生涯闲记:在SAP工作是什么体验

又有一段时间没更新自己的公众号了&#xff0c;为什么突然决定新开一篇SAP顾问生涯闲记系列的文章呢&#xff0c;是因为最近很荣幸地当选了SAP雇主品牌推广大使&#xff0c;作为SAP官方的推广大使在收获这份荣誉的同时&#xff0c;也承担了一些工作以及责任。 集结完毕︱SAP雇…

Flask_实现token鉴权

目录 1、安装依赖 2、实现代码 3、测试 源码等资料获取方法 1、安装依赖 pip install flask pip install pycryptodome 2、实现代码 import random import string import time import base64from functools import wrapsfrom flask import Flask, jsonify, session, req…

苍穹外卖day02——员工管理功能代码开发+分类管理代码导入

目录 新增员工——需求分析与设计 产品原型 接口设计: 数据库设计: 新增员工——代码开发 在Controller层中 在Service层中 在Mapper层中 功能测试 接口文档测试: 前后端联调测试: 新增员工——代码完善 ​编辑 第一个问题 第二个问题 员工分页查询 需求分析与设计 …

PostgreSQL考试难不难 ?

当涉及到PostgreSQL考试的详细难度&#xff0c;以下是一些可能涉及的主题和考点&#xff0c;这些主题在不同的考试中可能有所不同&#xff1a; 1.数据库基础知识&#xff1a;数据库的基本概念、关系型数据库模型、表、字段、主键、外键等。 2.SQL语言&#xff1a;对SQL语言的掌…

数据集——个人收集标注与使用过的数据集

前言 这是一个我个人在工作和学习中使用过以数据集的一部分&#xff0c;有语义分割&#xff0c;目标识别&#xff0c;人像抠图等几个大类&#xff0c;这只是我用过数据集中的一部分&#xff0c;这些数据集有小一部分是来源自网络&#xff0c;很大一部分都是我自己收集。 一、…

【动手学深度学习】--05.权重衰退

文章目录 权重衰退1.原理1.1使用均方范数作为硬性限制1.2使用均方范数作为柔性限制1.3对最优解的影响1.4参数更新法则 2.从零开始实现权重衰退2.1初始化模型参数2.2定义L2范数惩罚2.3训练2.4忽略正则化直接训练2.5使用权重衰退 3.简洁实现 权重衰退 学习视频&#xff1a;权重衰…

在网格化数据集上轻松执行 2D 高通、低通、带通或带阻滤波器研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

ModaHub魔搭社区:AI原生云向量数据库Zilliz Cloud设置白名单

目录 前提条件 操作步骤 下一步 在 Zilliz Cloud 中,白名单是针对项目的安全设置,适用于项目下的所有集群。设置白名单后,仅白名单中的 IP 地址可以访问您项目下的所有集群。白名单能够有效降低受到恶意攻击的风险 本教程将介绍如何设置白名单。 前提条件 确保满足以…

EasyX测试布局代码

#include <iostream> #include <algorithm> #include <graphics.h> // 引用图形库头文件 #include <conio.h> #include <unordered_map> #include <Windows.h> #include "layout/LayoutSystem.h"#define DEFAULT_PANELS_LAYOUT…

谈二级索引

前提&#xff1a; 在数据库中&#xff0c;1、索引分为聚簇索引和非聚簇索引两类。2、所有索引的数据结构都是树&#xff0c;查找树上的节点数据时通过用二分法来锁定数据范围&#xff0c;指定数据排序的规则&#xff0c;比如&#xff1a;有小到大&#xff0c;对比之后最终确定…

Sequencer使用心得

在关卡序列中设置了触发蓝图的关键帧&#xff0c;为什么播放的时候没有触发蓝图事件呢&#xff1f; 在关卡序列中触发蓝图&#xff0c;一般是将蓝图添加到轨道中&#xff0c;设置触发器&#xff0c;在对应的关键帧中&#xff0c;绑定蓝图事件。 一般的话&#xff0c;点击播…

栈、队列、优先级队列详解【c++】

目录 &#x1f3c0;stack的介绍和使用⚽stack的介绍⚽stack的使用 &#x1f3c0;queue的介绍和使用⚽queue的介绍⚽queue的使用 &#x1f3c0;priority_queue的介绍和使用⚽priority_queue的介绍⚽priority_queue的使用 &#x1f3c0;总结 &#x1f3c0;stack的介绍和使用 ⚽s…

尝试-InsCode Stable Diffusion 美图活动一期

一、 Stable Diffusion 模型在线使用地址&#xff1a; https://inscode.csdn.net/inscode/Stable-Diffusion 二、模型相关版本和参数配置&#xff1a; 活动地址 三、图片生成提示词与反向提示词&#xff1a; 提示词&#xff1a;realistic portrait painting of a japanese…

OPENMV的形状和颜色组合识别

使用openmv&#xff0c;通过阈值颜色和形状来去真假宝藏。调试过程发现颜色的阈值比较重要&#xff0c;因为不准的话&#xff0c;它会把一些颜色相近的物体也识别了。识别的精度有待提高&#xff0c;可以使用YOLOV5来精确识别&#xff0c;奈何本人没精力来弄这个。 打开机器视觉…

Proxmox VE 为 Windows 虚拟机添加硬盘遇到的问题

环境&#xff1a;PVE 8.x、Windows 11/Windows Server 2019 &#x1f449;问题一&#xff1a; 为 windows 虚拟机添加磁盘&#xff0c;重启虚拟机后&#xff08;在 windows 系统中重启&#xff09;磁盘未能生效&#xff0c;并显示为橘色。 ❗橘色 意味需要重启VM才能生效&…

BIO实战、NIO编程与直接内存、零拷贝深入辨析-02

网络通信编程基本常识 什么是 Socket &#xff1f; Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层&#xff0c;它是一组接口&#xff0c;一般由操作 系统提供。在设计模式中&#xff0c;Socket 其实就是一个门面模式&#xff0c;它把复杂的 TCP/IP 协议处理和…