【多线程】初步认识Thread类及其应用

news2024/9/22 3:51:45

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


        上篇文章我们简单介绍了什么是进程与线程,以及他们之间的区别与联系,实际应用中还是以多线程编程为主的,所以这篇文章就让我们更加深入地去剖析多线程编程的具体应用吧

目录

一、初识Thread类

1、创建线程

(1)继承Thread类

(2)实现Runnable接口

(3)匿名内部类

2、多线程的优势-增加运⾏速度

二、 Thread 类及常⻅⽅法

1、 Thread 的常⻅构造⽅法

2、 Thread 的⼏个常⻅属性

三、线程的状态

四、线程的核心操作

1、启动一个线程-start()

2、获取当前线程引用

3、休眠当前线程

4、线程的中断(终止)

5、线程等待-join()

总结


一、初识Thread类

⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.
Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾使⽤(例如 Linux 的 pthread 库).

1、创建线程

Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装
接下来我们就来看一下创建线程的几种写法吧:

(1)继承Thread类

编写的MyThread需要继承Thread类,不需要导包,因为Thread类是java.lang中内置的类。

继承不是主要目的,主要是为了重写Thread类中的run方法,在其中写入所创建线程需要执行的逻辑语句

若要让线程运行,需先实例化编写的MyThread类,接着调用start方法就会在进程内部创建一个新的线程,新的线程就会执行刚才run里的代码。

具体代码如下:

class MyThread extends Thread{
    //重写run方法
    @Override
    public void run(){
        //线程执行的逻辑
        System.out.println("Hello World!");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        //创建线程
        myThread.start();
    }
}

这个代码,运行起来是一个进程,但这个进程包含了两个线程。
1、调用main方法的线程被称为“主线程”,之前提过一个进程中至少有一个线程,这个线程就是主线程
2、调用myThread.start()方法时会手动创建一个新的线程
主线程和新线程会并发/并行地在CPU上运行
不过,上述代码还不能很好地体现多线程编程的并发性与随机性,接下来用一个更加形象的代码表示一下:
class MyThread extends Thread{
    //重写run方法
    @Override
    public void run(){
        while (true){
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t=new MyThread();
        //创建线程
        t.start();
        while (true){
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

可以发现,多个线程之间,谁先去CPU上调度执行,这个过程是 “不确定的”,这个调度顺序取决于操作系统内核里的 “调度器”,调度器里有一套规则,但是对于应用程序开发,无法进行干预,也无法察觉,因此把这个过程近似于 “随机”,多线程的运行调度也被称之为 “抢占式执行”

注意:

上述代码中,并没有直接手动调用run方法,但是也被执行了。像run这种,用户手动定义了,但是没有手动调用,最终被系统/库/框架调用执行了的方法,被称为“回调函数(call back)”

(2)实现Runnable接口

Runnable就是用来描述“要执行的任务”是什么
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello Runnable!");
    }
}
public class Main {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread myThread=new Thread(runnable);
        //创建线程
        myThread.start();
    }
}

通过Thread创建线程,而线程要执行的任务是通过Runnable来描述的,而不是通过Tread自己来描述,这样能起到一定的“解耦合”的作用,便于代码后期维护。

Runnable只是描述了一个任务,并不与“线程”强相关,后续执行这个任务的载体可以是线程,也可以是其他东西,比如线程池、虚拟线程(协程)等,一定程度上提高了代码的复用率。

对⽐上⾯两种⽅法:
继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
实现 Runnable 接⼝, this 表⽰的是 MyRunnable 的引⽤. 需要使⽤Thread.currentThread()

(3)匿名内部类

匿名指没有类名,内部类指定义在其它类内部的类,匿名内部类一般就是“一次性”使用的类,用完就丢掉,相对来说内聚性会更好一些

匿名内部类创建 Thread ⼦类对象
// 使⽤匿名类创建 Thread ⼦类对象
Thread t=new Thread(){
    @Override
    public void run() {
        System.out.println("使⽤匿名类创建 Thread ⼦类对象");
    }
};
这个方法本质上和(1)是一致的,具体原理如下:
1、定义匿名内部类,这个类是Thread的子类
2、类的内部,重写了父类的run方法
3、创建了一个子类的实例,并把实例的引用赋值给了t
匿名内部类创建 Runnable ⼦类对象:
//使⽤匿名类创建 Runnable ⼦类对象
Thread t2=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
    }
});

lambda表达式创建 Runnable 子类对象:

Thread t3=new Thread(()-> System.out.println("使⽤lambda表达式创建 Runnable ⼦类对象"));
Thread t4=new Thread(()->{
      System.out.println("使用lambda表达式创建 Runnable 子类对象");
});

lambda表达式本质上就是匿名内部类的更简化的写法,很多时候,写匿名内部类都不是为了写“类”,而是写类中的“方法”,而lambda就可以避开类而直接描述其中的run方法


2、多线程的优势

可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
使⽤ System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
serial 串⾏的完成⼀系列运算. concurrency 使⽤两个线程并⾏的完成同样的运算

public class ThreadAdvantage {
    // 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
    private static final long count = 10_0000_0000;

    public static void main(String[] args) throws InterruptedException {
        // 使⽤并发⽅式
        concurrency();
        // 使⽤串⾏⽅式
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();

        // 利⽤⼀个线程计算 a 的值
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a--;
                }
            }
        });
        thread.start();
        // 主线程内计算 b 的值
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        // 等待 thread 线程运⾏结束
        thread.join();

        // 统计耗时
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("并发: %f 毫秒%n", ms);
    }

    private static void serial() {
        // 全部在主线程内计算 a、b 的值
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a--;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long end = System.nanoTime();
        double ms = (end - begin) * 1.0 / 1000 / 1000;
        System.out.printf("串⾏: %f 毫秒%n", ms);
    }
}

二、 Thread 类及常⻅⽅法

Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。

1、 Thread 的常⻅构造⽅法

代码事例:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

如果不给线程起名字,那么默认就会是叫做Thread-0,Thread-1……

给线程起名字,并不会影响线程的执行效果,不过起一个合适的名字,更有利于调试程序

ThreadGroup线程组是把多个线程放在一组里,方便统一地设置线程的一些属性。不过现在很少会使用线程组了,线程相关属性用到也不多,更多的是会使用“线程池”

2、 Thread 的⼏个常⻅属性

ID 是JVM自动分配的,是线程的唯⼀标识,不同线程不会重复
名称是各种调试⼯具⽤的,Thread对象的身份标识
状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
优先级⾼的线程理论上来说更容易被调度到
关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
线程的中断问题,下⾯我们进⼀步说明

关于前台线程与后台线程:

后台线程:如果这个线程执行过程中,不能阻止进程的结束,(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就被称为“后台线程”,也被称为“守护线程”。

前台线程:如果某个线程在执行过程中,能够阻止进程结束,就被称为“前台线程”

一个线程中可以有多个前台线程(创建的线程默认是前台线程),必须所有的前台线程都结束,进程才能结束

关于线程是否存活:

isAlive()返回值为true表示还存活,false表示线程没了

代码中,new Thread对象的生命周期与内核中实际线程的生命周期不一定是一致的,可能会出现Thread对象存在但是内核中线程不存在的情况,如:

(1)调用start前,此时内核中还未创建线程

(2)线程中的run执行完毕,内核的线程销毁,但此时Thread对象任然存在

注意:不存在Thread对象不存在,而线程还存在的情况


三、线程的状态

线程的状态是⼀个枚举类型 Thread.State,反映了线程生命周期的不同阶段,了解这些更有利于对线程的调试与优化,以下是线程的六种主要状态:
NEW(初始):
  • Thread对象已创建,但是内核的线程还没有(还未调用start方法
  • 安排了⼯作, 还未开始⾏动
RUNNABLE(可运行):
  • 线程的start方法一旦被调用就会进入RUNNABLE状态
  • 就绪状态,可能正在CPU上运行,也可能在等待CPU分配资源以便运行
BLOCKED(阻塞):
  • 因为锁竞争而引起的阻塞等待的状态
  • 当线程试图获取某一对象的锁,而该对象的锁正被其它线程占有时,就会进入BLOCKED状态
  • 一般指线程因同步操作(synchronized)而被阻塞的状态
WAITING(等待):
  • 当线程调用了Object.wait(),Thread.join(),LockSupport.park()等方法时,就会进入WAITING状态
  • 此时线程不会争夺CPU资源,一直等待直到其它线程发出对应的通知信息(如notify()或notifyAll())时重新恢复RUNNABLE状态
  • 没有超时时间的阻塞等待,如果没有收到通知将会一直等待下去
TIMED_WAITING(超时等待):
  • 与WAITING状态类似,但是是有超时时间的等待
  • 当调用Object.wait(long timeout),Thread.join(long),Thread.sleep(long millis),LockSupport.parkNanos(),LockSupport.parkUntil()等方法时,线程会进入TIMED_WAITING状态
  • 线程将会等待直到被唤醒或超时
TERMINATED(终止):
  • 当线程的run方法执行完毕或者由于某些原因(如抛出为捕获的异常)而提前结束时,线程进入TERMINATED状态
  • 当前Thread对象虽然还在,但是内核的线程已经被销毁了(线程已经结束了)
  • 终止的线程无法被重启

可以形象的类比于以下状态:


上述线程状态可以通过jdk自带的jconsole来观察:

学习线程的状态,主要就是为了调试与优化,比如遇到某个线程没有正常运行时,就可以观察对应线程的状态,来确定是否是由于一些原因导致线程进入了阻塞状态


四、线程的核心操作

1、启动一个线程-start()

之前我们已经看到了如何通过覆写 run ⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。
覆写 run ⽅法是提供给线程要做的事情的指令清单
线程对象可以认为是把 李四、王五叫过来了
⽽调⽤ start() ⽅法,就是喊⼀声:”⾏动起来!“,线程才真正独⽴去执⾏了。

调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程
注意要区分好 runstart的关系:
run:线程的入口,描述了线程 要执行的任务
start:调用了 系统api,在系统内核中 真正创建了线程(创建PCB加入到链表中)
也就是说,如果直接调用run方法,虽然也能执行run方法内的程序,但也只会在调用它的线程中执行,是不会创建新的线程的,这样就背离多线程编程的初心了
注意
start对于一个Thread对象只能调用一次,java中的Thread对象与内核中的线程是“一对一”的关系,因此,不存在一个线程终止后,再通过调用start重新执行的情况
      这是由于一个Thread对象能够对应到多个线程的话,管理起来就会非常麻烦,JVM的设计实现就会非常复杂了。因此java中希望一个Thread对象只能对应到系统中的一个线程,这样在调用start后就可以通过线程的状态来判断是否能成功创建。
      如果一个Thread对象是没有调用过start的,此时就会处于NEW状态,此时调用start就可以顺利地执行创建出线程了。而如果已经调用过start,线程就会进入其它状态,只要不是处于NEW状态,接下来执行start都会抛出异常了

2、获取当前线程引用

想在某个线程中,获取到自身的Thread对象的引用,就可以通过currentThread()来获取

任何线程都可以通过这样的操作获取线程的引用

3、休眠当前线程

也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

线程执行sleep,就会使这个线程不参与CPU调度,从而把CPU资源让出来,给别人使用。像sleep这样的操作也被称之为“放权”,放弃使用CPU的权利

在有些开发场景中,如果某个线程CPU占用率过高,就可以通过sleep来改善。虽然线程的优先级就可以对其产生影响,但影响是比较有限的,通过sleep可以更加明显地影响到CPU占用

4、线程的中断(终止)

在工作中,我们可能会因为领导的一通电话,而不得不停下手头的工作,去做别的事。线程运行时可能也会遇到类似问题,有时我们可能会因为某些原因而需要提前结束线程的运行,该如何停止呢,这就涉及到停止线程的方式了。

如果有两个线程a和b,b正在运行,a想要b结束运行,其实核心就是a要想办法让b的run方法更快地执行完毕,此时b自然就结束了。而不是说b的run执行一半,a直接强行把b结束了。java中结束线程的方法是比较“温柔”的,并不是直接粗暴的。因为如果强制结束某个线程的话,可能导致其逻辑未完全执行,对应的结果数据是个“半成品”,从而影响程序最终的结果,这样肯定是不合理的。

1、一个简单的做法是使用自定义的标志位

public class Demo {
    //设置全局变量isQuit作为标志位
    public static boolean isQuit=false;

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while (!isQuit){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(4000);
        System.out.println("main线程尝试终止 t 线程");
        //通过修改isQuit变量的值,就能终止 t 线程的执行了
        isQuit=true;
    }

}

注意,如果把isQuit变成main方法的局部变量,就会出现以下情况:

我们可以看到lambda语句中的isQuit标红报错了。这与lambda表达式/匿名内部类的变量捕获操作有关。

isQuit与lambda表达式定义在一个作用域内,lambda的内部是可以访问到lambda外部(与lambda同一作用域)的变量的。但是lambda的变量是有要求的,能够捕获的变量得是final或者事实final(即虽没有final修饰,但并没有人修改),而上述代码中的isQuit变量被修改过,不是final/事实final,导致lambda表达式无法通过变量捕获操作获取到它,从而导致程序出现了错误

而当把isQuit写成成员变量后,就成了内部类访问外部类的成员变量,这本就是合法的,因此就不会出现问题了


2、使用Thread自带的interrupted作为标志位

 

Thread中有一个boolean类型的成员变量interrupted,它的初始值为false,表示未被终止,一旦其它线程调用了interrupt方法,就会设置上述标志位值为true,表示已被终止。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //下列的 lambda 的代码在编译器眼里,出现在 Thread t 的上方
        //此时 t 还没有被定义
        Thread t=new Thread(()->{
            //先获取到线程的引用
            Thread currentThread = Thread.currentThread();
            //两种判断方式都行
            while (!currentThread.isInterrupted()){
            //while(!Thread.interrupted())
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        //在主线程中,控制 t 线程被终止,设置上述标志位
        t.interrupt();
    }
}

但是执行上述代码我们会发现程序运行是有问题的:

可以看出程序运行抛出了一个RuntimeException异常。主要是由于判断isInterrupted()和执行打印操作的速度过快,因此在整个循环中,主要时间都是处于sleep状态中,main调用interrupt()时,不仅仅会设置标志位,还会把sleep给唤醒,假如sleep刚执行了100ms,还剩900ms,此时interrupt被调用,sleep就会被强制唤醒,并且抛出interruptedException异常

又由于catch中的代码默认再次抛出了一个RuntimeException异常,而这个异常没有被处理,就会导致直接抛到JVM层面,使得进程直接异常终止了

这时我们尝试将代码改为:

这样不抛出新的异常而是输出一段语句是否就能让程序正常运行呢?

显然是不能的,我们可以看到在执行catch操作后,线程并没有被终止,仍然在不断地运行输出。

这是由于当sleep等阻塞函数被唤醒后,会清空刚刚设置的interruptded标志位,这样在线程的下次循环判断时,程序就会认为标志位任未被设置,从而继续执行下去了。

此时,如果想要结束循环,结束线程,就需要在catch中加上return/break语句:

这样,线程就能正常被终止了:

出现这样的现象主要是由于java中,线程终止是一个相对“温柔”的过程,并不是强行就终止。当a线程想让b线程终止时,b可以自行决定,是否要终止/是立即还是稍后,这些都由b线程内部的代码来决定,其他线程无权干涉。

比如:

(1)如果b线程想无视a线程的终止请求,就直接在catch中啥也不做,b仍然会继续执行

(2)如果b线程相要立即结束,就在catch中写入return或break,此时b线程就会立刻结束

(3)如果b线程想要稍后结束,就可以在catch上写入一些其他逻辑(如释放资源,清理一些数据,提交一些结果……等收尾工作),这些逻辑完成后,再进行return/break操作

这些全都得益于sleep这类阻塞方法强制唤醒时会清除标志位,才能让b做出各种选择,否则b将被强制结束,无法写出让程序继续执行的代码了。这样可以给程序员更多的操作空间


5、线程等待-join()

操作系统针对多个线程的执行,是一个“随机调度”,抢占式执行的过程。线程的调度执行是随机的,我们无法确定两个线程的调度顺序,但是可以控制谁先结束谁后结束。

线程等待就是确定两个线程的结束顺序,通过让后结束的线程等待先结束的线程执行,进入阻塞状态,直到先结束的线程执行完毕,此时阻塞解除,后结束的线程开始执行。这样就能使两个线程的结束顺序得以确定

这时就可以使用join关键字实现线程等待

比如有两个线程a,b,此时在a线程中调用b.join,意思就是让a线程等待b线程先结束,a再继续执行。通俗的来讲,就相当于是让b插入到a线程的执行过程中

不过也要注意a和b本质上还是两个线程,依旧是并发执行,只是确定了结束顺序

代码示例:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            System.out.println("t 线程开始执行");
            for (int i = 0; i < 3; i++) {
                System.out.println("这是线程 t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t 线程执行结束");
        });
        t.start();
        //让主线程等待 t 线程
        System.out.println("main 线程开始等待");
        t.join();
        System.out.println("main 线程等待结束");
    }
}

可以很明显地看到主线程是先开始执行的,但当执行到t.join()语句时,主线程开始阻塞等待 t 线程的执行,当 t 线程执行结束后,join才会返回,主线程才会继续执行后续代码。

不过如果 t 线程先执行完毕了,然后主线程才开始join,此时主线程不会出现阻塞等待,而是会正常执行:

注意

        上述操作都是无参数的join方法,就是“死等”,只要被等待的线程没有执行完毕,就会一直阻塞等待。这并不是一个好的选择,因为一旦被等待的线程代码出现bug,可能导致该线程迟迟无法结束,从而使等待线程一直阻塞而无法继续执行其它操作了

方法如下:

可以在join方法中加入参数来确定等待时间,如果等待时间超过设定时间,就会停止等待,退出阻塞状态继续执行后续代码了。


方法汇总:

 

那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

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

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

相关文章

深入理解Python range()函数与for循环的完美配合

文末赠免费精品编程资料~~ 今天我们要聊的是一个超级实用且基础的功能组合——range() 函数和 for 循环。无论你是刚开始学习Python&#xff0c;还是已经有点基础&#xff0c;这篇文章都将帮助你更深入地理解和掌握这两个工具。 1. 什么是 range()&#xff1f; range() 是Py…

YOLOV8网络结构|搞懂head

head里面的模块之前在backbone出现过的&#xff0c;有conv和c2f&#xff0c;还有concat是新的&#xff0c;就是用来做连接的&#xff0c;就是把几个通道的输出给连接起来&#xff0c;有C,H,W三个维度的&#xff0c;就看最后连接的参数&#xff0c;是按照哪个方向把他们加起来&a…

【C++从小白到大牛】布隆过滤器

布隆过滤器的提出 布隆过滤器概念 布隆过滤器的原理&#xff1a; 布隆过滤器的运作的总体过程&#xff1a; 布隆过滤器的插入 原码 布隆过滤器的查找 布隆过滤器删除 关于布隆过滤器的面试题&#xff1a; 1. 给两个文件&#xff0c;分别有100亿个query&#xff0c;我们…

EmguCV学习笔记 VB.Net 4.2 二值化

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 教程VB.net版本请访问&#xff1a;EmguCV学习笔记 VB.Net 目录-CSDN博客 教程C#版本请访问&#xff1a;EmguCV学习笔记 C# 目录-CSD…

K8S资源之PVPVC

概念 类似于Docker的数据卷挂载&#xff0c;将Pod中重要的文件挂载到宿主机上&#xff0c;如果Pod发生崩溃等情况自愈时&#xff0c;保证之前存储的数据没有丢失。 如上图中&#xff0c;将各个Pod中的目录挂载到存储层&#xff0c;如果Pod宕机后自愈均从存储层获取之前的数据…

STM32——I2C和SPI波形分析

波形分析 I2C波形 //写命令 void OLED_WR_CMD(uint8_t cmd) { HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x00,I2C_MEMADD_SIZE_8BIT,&cmd,1,0x100); } //写数据 void OLED_WR_DATA(uint8_t data) { HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x40,I2C_MEMADD_SIZE_8BIT,&am…

【Linux线程】线程的深度解析(线程是什么?线程与进程区别是什么?)

目录 一、前言 二、 什么是线程 &#x1f4a7;线程的引入&#x1f4a7; &#x1f4a7;线程的基本概念 &#x1f4a7; &#x1f4a7;线程的理解 &#x1f4a7; &#x1f4a7;进程与线程的关系&#x1f4a7; &#x1f4a7;程序如何划分&#xff08;重拾页表、见一下LWP&…

行业分析---AI时代是不断更新自身技术还是会利用新技术?

1 背景 最近有两个热点新闻&#xff0c;&#xff08;1&#xff09;“孟晚舟建议不要选和机器竞争的职业&#xff0c;根本不是它的对手”&#xff1b;&#xff08;2&#xff09;“周鸿祎建议萝卜快跑把无人出租车卖给司机&#xff0c;可实现三赢”。 技术的变革在互联网领域是比…

QT翻金币小游戏(含音频图片文件资源)

目录 QT翻金币小游戏 音频图片资源文件获取 效果展示 图片 视频 实现代码 main.cpp mymainwindow.h mymainwindow.cpp startscene.h startscene.cpp selectscene.cpp playscene.h playscene.cpp mypushbutton.h mypushbutton.cpp dataconfig.h dataconfig.cpp QT…

音频剪辑用什么工具?试试这三款

音乐&#xff0c;是情感的传递者&#xff0c;是灵魂的慰藉。作为一名音乐人&#xff0c;我一直在探索如何更好地捕捉和表达音乐的精髓。在这个数字化的时代&#xff0c;音频剪辑软件成为了我们表达创意的重要工具。今天&#xff0c;我想从一个音乐人的角度&#xff0c;分享我使…

C语言每日好题(3)

有任何不懂的问题可以评论区留言&#xff0c;能力范围内都会一一回答 #define _CRT_SECURE_NO_WARNING #include <stdio.h> #include <string.h> int main(void) {if ((strlen("abc") - strlen("abcdef")) > 0)printf(">\n")…

CentOS 7 下载/安装

下载 centos安装包下载_开源镜像站-阿里云centos安装包是阿里云官方提供的开源镜像免费下载服务&#xff0c;每天下载量过亿&#xff0c;阿里巴巴开源镜像站为包含centos安装包的几百个操作系统镜像和依赖包镜像进行免费CDN加速&#xff0c;更新频率高、稳定安全。https://mir…

SpringBoot(一)

1.Spring Boot概要 1.1 SpringBoot介绍 随着动态语言的流行&#xff08;Ruby、Scala、Node.js&#xff09;, Java的开发显得格外的笨重&#xff1b;繁多的配置、低下的开发效率、复杂的部署流程以及第三方技术整合难度大。 在上述环境下&#xff0c;Spring Boot由此诞生&#…

每天五分钟计算机视觉:搭建人脸识别的Siamese深度神经网络模型

本文重点 前面的一篇文章中介绍了关于一次学习的问题,解决一次学习问题的关键在于学习到一个函数d,这个d可以计算出两张图片中的人脸是不是同一个人。那么我们需要搭建什么样的神经网络才可以让模型学习出这样的函数d呢?本文我们介绍一下Siamese神经网络结构,它可以帮助我…

快速上手体验MyPerf4J监控springboot应用(docker版快速开始-本地版)

使用MyPerf4J监控springboot应用 快速启动influxdb时序数据库日志收集器telegrafgrafana可视化界面安装最终效果 项目地址 项目简介: 一个针对高并发、低延迟应用设计的高性能 Java 性能监控和统计工具。 价值 快速定位性能瓶颈快速定位故障原因 快速启动 监控本地应用 idea配…

BeagleBone Black 上手

芯片特性 板级功能 资源内存 SDRAM 512MB DDR3L 800MHZ A single 256Mb x16 DDR3L 4Gb (512MB) memory device is used. The memory used is one of two devices: MT41K256M16HA-125 from Micron D2516EC4BXGGB from Kingston It will operate at a clock frequency of 400M…

DDD领域驱动设计的原理与实践

目录 什么是DDD领域驱动设计&#xff1f; 定义与概念&#xff1a; 核心思想&#xff1a; 核心概念&#xff1a; 核心原则&#xff1a; 优势与应用&#xff1a; 与微服务架构和传统三层架构的关系&#xff1a; 理解领域模型 举例 统一语言&#xff08;Ubiquitous Langu…

【C++11】入门基础

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;C从入门至进阶 这里将会不定期更新有关C/C的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 文章目录…

基于Kotlin Multiplatform实现静态文件服务器(三)

Expect 和 Actual expect 关键字用于定义一个多平台通用的声明&#xff0c;即该声明在所有平台上都可用&#xff0c;并且需要在特定平台上实现。actual 关键字通常与 expect 关键字配合使用&#xff0c;用于定义多平台通用的接口和函数&#xff0c;从而允许在不同的平台上使用…

PyTorch--深度学习

onux部署功能 cpu运行时间 3. 自动求导 求导结果为&#xff1a;2 1 1