Java-多线程基础及线程安全

news2024/11/28 21:57:49

文章目录

  • 1. 线程的状态
    • 1.1 观察线程的所有状态
    • 1.2 观察线程的转态和转移
  • 2. 多线程带来的风险, 线程安全
    • 2.1 观察线程不安全
    • 2.2 线程安全的概念
    • 2.3 线程不安全的原因
    • 2.4解决上述代码的线程不安全问题
  • 3. synchronized 关键字
    • 3.1 synchronized 的特性
    • 3.2 synchronized 使用示例
    • 3.3 volatile 关键字
  • 4. wait 和 notify
    • 4.1 wait() 方法
    • 4.2 notify() 方法
    • 4.3 notifyall() 方法

1. 线程的状态

1.1 观察线程的所有状态

public class ThreadDemo1 {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

在这里插入图片描述
线程的状态是一个枚举类型 Thread.State

  • NEW (新建状态) 新建了一个线程, 但是还没有启动, 此时线程还没有分配任何资源, 就是安排好了工作, 还没有开始行动.
  • RUNNABLE (就绪状态) 线程被启动后, 就绪状态也称为可运行状态, 线程已经被分配了处理器资源, 并被操作系统调度到处理器上运行, 等待CPU时间片, 也就是可工作的状态, 进而可分为正在工作中和即将开始工作.
  • BLOCKED (阻塞状态) 当一个线程试图获取一个内部的对象锁(不是java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入BLOCKED状态。BLOCKED状态的线程不会进入队列等待,而是等待直到该锁被释放。一旦锁被释放,BLOCKED状态的线程就会进入RUNNABLE状态,可以被调度运行.
  • WAITING (等待状态) 线程调用了wait()方法后,线程就进入等待状态,继续执行wait()后面的代码。在等待过程中,线程不会释放自己占用的资源。如果其他线程调用了该线程的notify()方法,或者调用notifyAll()方法,该线程就会从等待状态进入RUNNABLE状态。如果既没有notify也没有notifyAll调用,那么这个线程将永远不会从等待状态退出.
  • TIME_WAITING (超时等待状态) 线程调用了Thread.sleep()或Thread.join()方法后,线程就进入超时等待状态。与WAITING不同,超时等待状态的线程会在指定的时间后自动从等待状态进入RUNNABLE状态。
  • TERMINATED (终止状态) 线程已经执行完毕或者异常结束,此时线程已经不属于程序的一部分.

在这里插入图片描述

1.2 观察线程的转态和转移

1. 关注 NEW , RUNNABLE , TEMINATED 状态的转换

  • 使用 isAlive 方法判定线程的存货状态
public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000_0000; i++) {
                }
            }
        },"张三");
        System.out.println(thread.getState()); // 启动线程之前
        thread.start();  //启动线程
        while (thread.isAlive()) {
            System.out.println(thread.getState());
        }
        System.out.println(thread.getState());
    }
}

在这里插入图片描述
在这里插入图片描述

运行这个代码我们可以清楚的看到启动线程之前, 线程处于 NEW 状态, 线程启动后开始工作处于 RUNNABLE 状态, 工作完成后为 TERMINATED 状态

2. 关注 WAITING, BLOCK , TIMED_WAITING 状态的转化

public class ThreadDemo3 {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (object) {
                while (true) {
                    try {
                        System.out.println("t2没执行");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        },"t1");
        thread1.start();

        Thread thread2 = new Thread(() -> {
            synchronized (object) {
                System.out.println("hello");
            }
        },"t2");
        thread2.start();
    }
}

使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
在这里插入图片描述

在这里插入图片描述
下面我们修改代码, 将上面的sleep() 换成 wait()

public class ThreadDemo4 {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (object) {
                while (true) {
                    System.out.println("t2没执行");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        },"t1");
        thread1.start();

        Thread thread2 = new Thread(() -> {
            synchronized (object) {
                System.out.println("hello");
            }
        },"t2");
        thread2.start();
    }
}

在这里插入图片描述
使用 jconsole 可以看到 t1 的状态是 WAITING

由上面可得:
BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

WAITING(等待状态):当线程进入WAITING状态时,它放弃当前锁,并等待其他线程执行特定的操作(如notify或notifyAll)。如果没有其他线程通知或唤醒它,它将一直处于等待状态,直到具有相同锁的其他线程完成其任务。
TIMED_WAITING(定时等待状态):这是WAITING状态的一种特殊形式。当线程进入TIMED_WAITING状态时,它不仅会放弃当前锁,而且会在指定的时间内等待其他线程的通知或唤醒。如果在指定的时间内没有其他线程通知或唤醒它,那么它将自动回到RUNNABLE状态
注意:无论是WAITING还是TIMED_WAITING状态,线程都不会释放自己占用的资源(如内存),而是会一直等待直到被其他线程唤醒。此外,这两种状态都是在synchronized块或方法中进入的,所以需要释放锁才能继续执行

3. yield()
当线程执行yield()方法时,它会释放当前CPU时间片的控制权,并且操作系统会尝试将CPU分配给其他正在等待的线程。这使得其他线程有机会获得CPU时间片并执行它们的代码.
需要注意的是,yield()方法并不保证一定会将CPU时间片分配给其他线程,具体取决于操作系统的调度策略。如果操作系统认为当前线程仍然适合继续执行,那么它可能会忽略yield()的请求,继续执行当前线程.
yield()方法通常在编写多线程程序时使用,当一个线程已经完成了一些工作,但还没有到达可以继续执行的下一个条件时,可以使用yield()方法来主动放弃CPU时间片,以便其他线程可以执行.

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            while (true) {
                System.out.println("hello");
                Thread.yield();
            }
        });

        Thread thread2 = new Thread(() -> {
            while (true) {
                System.out.println("你好");
            }
        });
        thread1.start();
        thread2.start();
    }
}

按正常来说, 不使用Thread.yield() , 打印 “hello” 和 “你好” 的次数应该五五开, 但是使用这个Thread.yield()之后, “hello” 的数量就远远少于了"你好"了.

结论:yield() 不改变线程的状态, 但是会重新去排队.

2. 多线程带来的风险, 线程安全

2.1 观察线程不安全

我们先看下面一个代码

public class ThreadDemo6 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
             count++;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread1.join();
        System.out.println(count);
    }
}

在这里插入图片描述

2.2 线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线
程安全的

2.3 线程不安全的原因

修改共享数据
在这里插入图片描述

我们上述的代码中. 涉及多个线程对 count 的修改, 此时这个 count 是一个多线程能访问到的 “共享数据”
在多线程环境中对数据进行修改,会出现数据不一致的情况

原子性
在这里插入图片描述

但客户端A检查还有一张票的时候, 将票卖了. 还没有更新数据库时, 客户端B检查了票数, 发现大于1 , 又买了一张票, 这就出现一张票被卖出去两次的问题.

说到这里, 我们就得了解一下什么是原子性.
我们把一段代码想象成一个房间, 每个线程就是想进入房间的人, 如果A进入到房间里, 没有任何保护机制的话, 是不是B也可以进入房间啊, 如果B进去了话, 就打断了A的隐私, 这就是不具备原子性的.
那么我们应该如何解决这个问题呢, 那就是加锁 , A进入房间的时候, 加个锁,把门关上, 这样就保证了这段代码的原子性了.

一条Java语句不一定就是原子性的, 也不一定就只是一条命令
就比如上面的代码, count++, 其实是分三步完成的

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

可见性

可见性是指一个线程对共享变量值的修改, 能够及时被其他线程看到.

Java内存模型(JMM): Java虚拟机规范中定义了Java内存模型
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并
发效果.

在这里插入图片描述

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

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

初始情况下, 两个线程的工作内存内容一致.
在这里插入图片描述

一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定
能及时同步.
在这里插入图片描述
这个时候代码中就容易出现问题.

那我们就得考虑这两个问题了

  1. 为什么要整这么多内存呢?
  2. 为什么要这么麻烦的拷来拷去

为啥整这么多内存
实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.

为啥要这么麻烦的拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍).

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了

CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘.
CPU 的价格最贵, 内存次之, 硬盘最便宜

代码顺序性
JVM的优化:虽然Java规范规定了顺序性,但具体的JVM实现可能会有自己的优化策略,这也可能影响代码的执行顺序。例如,JVM可能对代码进行重排、优化等操作以提高性能。

2.4解决上述代码的线程不安全问题

public class Test {
    public static int count = 0;
    synchronized static void sum() {
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                sum();
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                sum();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

3. synchronized 关键字

3.1 synchronized 的特性

  1. 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

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

理解阻塞等待.

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁

这里我们要注意的是:

  • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
  1. 刷新内存

synchronized 的工作过程:

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

所以 synchronized 也能保证内存可见性的.

  1. 可冲入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
那什么是把自己锁死呢?
一个线程没有释放锁, 然后又尝试再次加锁.

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁, 这样的锁称为 不可重入锁

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

我们看下面代码

static class Counter {
  public int count = 0;
  synchronized void increase() {
    count++;
 }
  synchronized void increase2() {
    increase();
 }
}
  • ncrease 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)这个代码是完全没问题的. 因为 synchronized 是可重入锁.

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

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

3.2 synchronized 使用示例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

直接修饰普通方法:锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
  public synchronized void methond() {
 }
}

修饰静态方法: 锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
  public synchronized static void method() {
 }
}

修饰代码块: 明确指定锁哪个对象

锁当前对象

public class SynchronizedDemo {
  public void method() {
    synchronized (this) { 
   }
 }
}

锁类对象

public class SynchronizedDemo {
  public void method() {
    synchronized (SynchronizedDemo.class) {
   }
 }
}

3.3 volatile 关键字

volatile 能保证内存可见性

在这里插入图片描述

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

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

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

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

我们看一个代码

  • 创建两个线程 t1 和 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束.
import java.util.Scanner;

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

在这里插入图片描述

当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

t1 读的是自己工作内存中的内容.
当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.

如果给 flag 加上 volatile

public class Test2 {

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

在这里插入图片描述

volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

我们看上述的一个代码

public class Test {
    public static int count = 0;
    synchronized static void sum() {
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                sum();
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                sum();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

这个代码加锁后我们得到了我们想要的结果

  • 如果给 sum 方法去掉 synchronized
  • 给 count 加上 volatile 关键字.

public class Test3 {
    public static volatile int count = 0;
     static void sum() {
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                sum();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                sum();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

在这里插入图片描述
此时可以看到, 最终 count 的值仍然无法保证是 100000.

synchronized 既能保证原子性, 也能保证内存可见性

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

4. wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
    注意: wait, notify, notifyAll 都是 Object 类的方法.

4.1 wait() 方法

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 做的事情

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

wait 结束等待的条件

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常
public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
                System.out.println("等待中");
                object.wait();
                System.out.println("等待结束");
    }
    }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就
需要使用到了另外一个方法唤醒的方法notify()。

4.2 notify() 方法

notify 方法是唤醒等待的线程

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

下面我们看一个代码

public class Test6 {
    private static Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("wait 开始");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 结束");
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("notify 开始");
                lock.notify();
                System.out.println("notify 结束");
            }
        });

        thread1.start();
        thread2.start();
    }
}

在这里插入图片描述

4.3 notifyall() 方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

虽然是同时唤醒 多个线程, 但是这些线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

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

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

相关文章

【Leetcode】 450. 删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤&#xff1a; 首先…

数学小把戏 6174

Wills健身房的手牌编号就是存放衣服的柜子。 柜子是狭长的L或7型&#xff0c;竖着放刚够塞进双肩背包&#xff0c;偶尔我横过来塞进 L 型底座或7的顶柜。 尴尬来的比偶尔次数还是多一点。 在我换衣服时候&#xff0c;旁边的柜子要打开&#xff0c;压迫感陡然拉满。局促的空间…

黑马程序员 MySQL数据库入门到精通——进阶篇(1)

黑马程序员 MySQL数据库入门到精通——进阶篇&#xff08;1&#xff09; 1. 存储引擎1.1 MySQL体系结构1.2 存储引擎简介1.3 存储引擎特点1.3.1 InnoDB1.3.2 MyISAM1.3.3 Memory1.3.4 三种存储引擎对比 1.4 存储引擎选择 2. 索引2.1 索引概述&#xff08;Index Overview&#x…

css复合选择器

交集选择器 紧紧挨着 <template><div><p class"btn">Click me</p><button class"btn" ref"myButton" click"handleClick">Click me</button></div> </template> <style> but…

内存函数(memcpy、memmove、memset、memcmp)你真的懂了吗?

&#x1f493;博客主页&#xff1a;江池俊的博客⏩收录专栏&#xff1a;C语言进阶之路&#x1f449;专栏推荐&#xff1a;✅C语言初阶之路 ✅数据结构探索&#x1f4bb;代码仓库&#xff1a;江池俊的代码仓库&#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐ 文…

一键智能视频语音转文本——基于PaddlePaddle语音识别与Python轻松提取视频语音并生成文案

前言 如今进行入自媒体行业的人越来越多&#xff0c;短视频也逐渐成为了主流&#xff0c;但好多时候是想如何把视频里面的语音转成文字&#xff0c;比如&#xff0c;录制会议视频后&#xff0c;做会议纪要&#xff1b;比如&#xff0c;网课教程视频&#xff0c;想要做笔记&…

wdb_2018_2nd_easyfmt

wdb_2018_2nd_easyfmt Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8047000)32位只开了NX 这题get到一点小知识&#xff08;看我exp就知道了 int __cdecl __noreturn main(int argc, const char…

字节一面:深拷贝浅拷贝的区别?如何实现一个深拷贝?

前言 最近博主在字节面试中遇到这样一个面试题&#xff0c;这个问题也是前端面试的高频问题&#xff0c;我们经常需要对后端返回的数据进行处理才能渲染到页面上&#xff0c;一般我们会讲数据进行拷贝&#xff0c;在副本对象里进行处理&#xff0c;以免玷污原始数据&#xff0c…

ARP欺骗攻击实操

目录 目录 前言 系列文章列表 全文导图 1&#xff0c;ARP概述 1.1,ARP是什么&#xff1f; 1.2,ARP协议的基本功能 1.3,ARP缓存表 1.4,ARP常用命令 2&#xff0c;ARP欺骗 2.1,ARP欺骗的概述? 2.2,ARP欺骗的攻击手法 3&#xff0c;ARP攻击 3.1,攻击前的准备 3.2,…

数学建模Matlab之评价类方法

大部分方法来自于http://t.csdnimg.cn/P5zOD 层次分析法 层次分析法&#xff08;Analytic Hierarchy Process, AHP&#xff09;是一种结构决策的定量方法&#xff0c;主要用于处理复杂问题的决策分析。它将问题分解为目标、准则和方案等不同层次&#xff0c;通过成对比较和计算…

软件设计模式系列之二十——备忘录模式

备忘录模式目录 1 模式的定义2 举例说明3 结构4 实现步骤5 代码实现6 典型应用场景7 优缺点8 类似模式9 小结 备忘录模式是一种行为型设计模式&#xff0c;它允许我们在不暴露对象内部细节的情况下捕获和恢复对象的内部状态。这个模式非常有用&#xff0c;因为它可以帮助我们实…

HTML——列表,表格,表单内容的讲解

文章目录 一、列表1.1无序&#xff08;unorder&#xff09;列表1.2 有序&#xff08;order&#xff09;列表1.3 定义列表 二、表格**2.1 基本的表格标签2.2 演示 三、表单3.1 form元素3.2 input元素3.2.1 单选按钮 3.3 selcet元素 基础部分点击&#xff1a; web基础 一、列表 …

全面解析‘msvcp140.dll丢失的解决方法’这个问题

msvcp140.dll 是什么东西&#xff1f; msvcp140.dll 是 Microsoft Visual C 2015 Redistributable Package 中的一个动态链接库文件。它包含了 C运行时库中的函数和类&#xff0c;这些函数和类在开发 C应用程序时被广泛使用。msvcp140.dll 的主要作用是在 Windows 操作系统中提…

1.5.C++项目:仿mudou库实现并发服务器之socket模块的设计

项目完整版在&#xff1a; 一、socket模块&#xff1a;套接字模块 二、提供的功能 Socket模块是对套接字操作封装的一个模块&#xff0c;主要实现的socket的各项操作。 socket 模块&#xff1a;套接字的功能 创建套接字 绑定地址信息 开始监听 向服务器发起连接 获取新连接 …

WordPress外贸建站Astra免费版教程指南(2023)

在WordPress的外贸建站主题中&#xff0c;有许多备受欢迎的主题&#xff0c;如AAvada、Astra、Hello、Kadence等最佳WordPress外贸主题&#xff0c;它们都能满足建站需求并在市场上广受认可。然而&#xff0c;今天我要介绍的是一个不断颠覆建站人员思维的黑马——Astra主题。 …

【计算机网络】DNS原理介绍

文章目录 DNS提供的服务DNS的工作机理DNS查询过程DNS缓存 DNS记录和报文DNS记录DNS报文针对DNS服务的攻击 DNS提供的服务 DNS&#xff0c;即域名系统(Domain Name System) 提供的服务 一种实现从主机名到IP地址转换的目录服务&#xff0c;为Internet上的用户应用程序以及其他…

网页采集工具-免费的网页采集工具

在当今数字化时代&#xff0c;网页采集已经成为了众多领域的必备工具。无论是市场研究、竞争情报、学术研究还是内容创作&#xff0c;网页采集工具都扮演着不可或缺的角色。对于许多用户来说&#xff0c;寻找一个高效、免费且易于使用的网页采集工具太不容易了。 147SEO工具的强…

ElasticSearch更新数据后查不到的问题

一、前言 上一篇文章还是2个星期前写的&#xff0c;近段时间有点懒&#xff0c;本来这篇也不太愿意动笔写&#xff0c;但这两天关注数据&#xff0c;发现新的一年已经收获了4个粉丝&#xff0c;首先感谢大家的关注&#xff0c;我以后还是会尽量多写一点。这篇文章讲一下今天我…

从零手搓一个【消息队列】BrokerServer 创建核心类, 数据库设计与实现

文章目录 一、创建核心类1, 交换机2, 交换机类型3, 队列4, 绑定5, 交换机转发 & 绑定规则6, 消息7, 消息属性 二、数据库设计1, 使用 SQLite2, 使用 MyBatis2.1, 创建 Interface2.2, 创建 xml 文件 三、硬盘管理 -- 数据库1, 创建 DataBaseManager 类2, init() 初始化数据库…