线程安全(万字详解)

news2024/11/15 6:00:10

目录

线程安全

概念

用一段代码感受线程安全

线程安全问题的原因 

修改上述代码,使其线程安全

synchronized

synchronized使用方法

锁对象的规则

synchronized用法,代码展示

 monitor lock

sychronized的特性

java标准库中的线程安全类 

死锁

死锁的常见原因

多个线程多把锁,死锁的必要条件

多个线程多把锁死锁的解决方案

volatile

wait 和 notify

wait和notify概念

wait进行阻塞

wait方法的操作步骤

wait 和 sleep

练习题


线程安全

概念

多线程是抢占式执行的,执行具有随机性
如果没有多线程,代码的执行顺序是固定的,代码顺序固定,执行的结果也是确定的
有了多线程,由于多线程是抢占式执行的,代码顺序会出现很多变数
就需要保证在无数种线程调度的顺序情况下,代码的执行结果都是正确的
只要有一种情况不正确,代码执行结果不正确,就认为有Bug,线程不安全

用一段代码感受线程安全

这里我们先写一个有线程安全问题的代码,先定义1个Counter类,类里面有count这个成员变量,有一个add方法,可以对count进行自增操作
使用俩个线程,俩个线程分别针对count 来调用5W次add方法
最后输出count的值(预期结果10w)

class Counter{
    public int count;
    public  void add(){
            count++;
    }
}
public class ThreadDemo12 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //使用俩个线程,俩个线程分别针对count 来调用5W次add方法
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

为什么会出现这种bug?

count++操作,本质上分为3步

1.先把内存中的值读到cpu寄存器中 load
2.把cpu寄存器中的值进行 +1 运算  add
3.把寄存器中的值,同步到内存中      save

如果俩个线程并发执行count++,此时就相当于俩组或者多组load add save进行执行
此时不同的调度顺序,可能就会发生结果上的差异

由于线程调度的随机性,调度的顺序充满可能性,有无数种可能

这种情况就会出现线程安全问题了

这和脏读有点类似,其实就是事物 读 未提交,是一样的
相当于t1 读到的是一个 t2还没来得及提交的脏数据
于是就出现了脏读问题 

当前这个代码是否不出线程安全问题,结果真好是10w呢?

也是有可能的,比如出现的都是以下俩种情况

这个代码结果一定会大于5w吗?

不一定,假设俩个线程的调度出现,自增俩次,只增1次的效果,或者t1自增1次,t2自增了多次,最终结果还是增1

线程安全问题的原因 

1.根本原因:

抢占式执行,随机调度

2.代码结构:

多个线程同时修改同一个变量
一个线程,修改一个变量没事
多个线程读取同一个变量没事
多个线程修改多个不同的变量也没事
因此我们可以通过调整代码结构来规避线程安全问题
但是通常代码结构是源于需求的,不一定能改

3.原子性

如果修改操作是原子的,不容易发生线程安全问题
但如果修改操作不是原子的,就很可能发生线程安全问他

什么是原子性?

原子在计算机中表示为,不可拆分的基本单位
count++,可以拆分load add save 三个指令
单个指令就是原子的 例如上面的load
count++是三个指令,所以这个操作不是原子的

4.内存可见性(编译器优化出bug)

如果是一个线程对一个变量进行读操作,另一个线程对这个变量进行修改操作
此时读的值,不一定是修改后的值,读取变量的线程,没有感知到变量的变化

本质:

java程序里,有一个共用的内存,每个线程还有自己的cpu寄存器/缓存(catche)
t1线程进行读取的时候,只读取了cpu寄存器/catche里的值
t2线程进行修改的时候,先修改自己的cpu寄存器/catche里的值,再把cpu寄存器/catche里的值同步到内存中
但由于编译器优化,导致t1 没有重新的从主内存同步数据到cpu寄存器/catche中,读到的结果就是修改之前的结果

5.指令重排序(本质是编译器优化出bug了)

编译器觉得我写的代码不太好,就自作主张把代码调整了
保持逻辑不变的情况下,调整了代码的执行顺序,从而加快程序的执行效率

以上只是五个典型的原因,并不是全部,具体问题具体分析
原则:多线程运行代码,不出bug就是线程安全的

修改上述代码,使其线程安全

上述代码是一个典型的原子性问题,
我们可以通过 加锁 操作,把不是原子的转换成"原子的"

class Counter{
    public int count;
    public synchronized void add(){
            count++;
    }
}
public class ThreadDemo12 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //使用俩个线程,俩个线程分别针对count 来调用5W次add方法
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

我们上个代码出现线程安全的原因是
由于多个线程一起调用add方法,进行count++操作,可能就会导致俩个线程同时执行count++操作,t1线程count++操作还没执行完,还没把结果保存到内存中,t2线程也开始执行count++操作,就会出现调用了2次add,count只增1等等情况
现在我们通过对add方法进行加锁,每次有线程调用add方法,就会要对这个Counter对象进行加锁,
假如线程1调用add方法对Counter这个对象进行加锁,同时线程2想要调用add方法,也要对Counter对象进行加锁,由于一个对象只能被一个线程加锁,所以线程2只能阻塞等待,等待线程1把add方法执行完毕,线程1主动释放锁,这时线程2才能对Counter加锁,执行add方法
这样就保证了,add这个方法,每次最多只有一个线程在执行,也就是这里的原子性

简单来说,就是这里给add加锁的目的就是,保证了每次做多只有一个线程去执行add方法

操作系统里面的锁具有"不可剥夺"的特性,一旦一个线程获取到锁,除非主动释放,否则其它线程无法强占
一旦加锁之后,代码的执行速度是大大折扣的
虽然加锁之后,代码的执行速度慢了,但化生比单线程要快
只是在执行add这个方法的时候,串行执行了,除了add方法,for循环也是可以并发执行的
一个任务中,一部分可以并发,一部分串行,任然比所有代码都串行的执行速度快

synchronized

synchronized使用方法

1.修饰方法

(1)修饰普通方法     修饰普通方法,锁对象就是this

(2)修饰静态方法     修饰静态方法,锁对象就是类方法(刚刚代码的Counter)

2.修饰代码块         修饰代码块,手动指定锁对象

锁对象的规则

如果俩个线程针对同一个线程同一个对象加锁,就会出现锁竞争/锁冲突
一个线程先获取到锁(先到先得),另一个阻塞等待
等待到上一个线程解锁,才能获取锁成功
如果俩个线程针对不同对象加锁,此时不会发生锁竞争/锁冲突
这俩线程都能获取到各自的锁,不会再阻塞等待了


俩个线程,针对同一个对象,一个线程加锁,一个线程不加锁,此时也没有锁竞争

 比如一个线程调用add方法,同时另一个线程调用add2方法,此时不会发生阻塞等待 

synchronized用法,代码展示

 monitor lock

jvm给synchronized起了个名字,因此代码中有时候会出异常会有这个说法

sychronized的特性

(1)互斥

synchronized会起到互斥的效果,某个线程执行到某个对象的 synchronized时,其它线程如果也执行到同一个对象 sychronized就会阻塞等待
进入synchronized修饰的代码块,相当于加锁
退出synchronized修饰的代码块,相当于解锁

(2)可重入

一个线程针对同一个对象,连续加俩次锁,
如果不报错,那么这个锁就是可重入的,如果报错就是不可重入的

java标准库中的线程安全类 

如果多个线程操作同一个集合类,就需要考虑线程安全的问题

死锁

死锁的常见原因

(1)一个线程,一把锁,连续加俩次,如果锁是不可重入的,就会死锁

(2)俩个线程俩把锁,t1和t2各自针对 锁A 和锁 B,再尝试获取到对方的锁

下面用代码来演示

public class ThreadDemo13 {
    public static void main(String[] args) {
        Object cu = new Object();
        Object lajiaojiang = new Object();
        Thread xiaoming = new Thread(()->{
            synchronized (cu) {
                System.out.println("小明把醋拿到了");
                //保证小明拿到醋,小红拿到辣椒酱
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lajiaojiang) {
                    System.out.println("小明拿到醋和辣椒酱了");
                }
            }
        });
        Thread xiaohong = new Thread(()->{
            synchronized (lajiaojiang) {
                System.out.println("小红把辣椒酱拿到了");
                //保证小明拿到醋,小红拿到辣椒酱
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (cu) {
                    System.out.println("小红拿到醋和辣椒酱了");
                }
            }
        });
        xiaoming.start();
        xiaohong.start();
    }
}

 执行结果

用jconsole查看线程情况

 

针对死锁的问题,我可以通过jconsole这样的工具来进行定位
看线程状态和调用栈,就可以分析出代码在哪里死锁了

(3)多个线程多把锁(相当于2的一般情况) 

假设出现了一种极端情况,就会死锁,
同一时刻,所有的哲学家,拿起了左手的筷子
所有的哲学家,这时都拿不起右手的筷子(被其它哲学家拿了) ,都要等右边的哲学家把筷子放下
此时就会出现僵住了的情况,发生死锁

多个线程多把锁,死锁的必要条件

1.互斥使用: 线程1拿到了锁,线程2想获取锁,就要阻塞等待

2.不可强占:线程1拿到了锁之后,除非线程1主动释放,不能是线程2强行把锁获取到

3.请求和保持:线程1拿到锁A后,再次尝试获取锁B,A这把锁还是保持的(不会因为去获取锁B,就把锁A给释放了)

4.循环等待 线程1 获取到锁A ,再尝试获取锁B,线程2获取到锁B,在尝试获取锁A
线程1 在获取锁B 的时候,等待线程2 释放B,线程2在获取锁A 的时候,等待线程1释放A

多个线程多把锁死锁的解决方案

上述前三个条件都是sychronized这把锁的基本特性,因此想要不死锁,只能改变循环等待这个条件

代入到一开始写的代码
假设我们规定,醋的编号为1,辣椒酱的编号为2
按照从小到大的顺序获取锁
这样先获取到的一定是醋,小红获取不到就会阻塞等待
等待小明释放醋,再去获取醋

public class ThreadDemo13 {
    public static void main(String[] args) {
        Object cu = new Object();
        Object lajiaojiang = new Object();
        Thread xiaoming = new Thread(()->{
            synchronized (cu) {
                System.out.println("小明把醋拿到了");
                //保证小明拿到醋,小红拿到辣椒酱
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lajiaojiang) {
                    System.out.println("小明拿到醋和辣椒酱了");
                }
            }
        });
        Thread xiaohong = new Thread(()->{
            synchronized (cu) {
                System.out.println("小红把醋拿到了");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lajiajiang {
                    System.out.println("小红拿到醋和辣椒酱了");
                }
            }
        });
        xiaoming.start();
        xiaohong.start();
    }
}

 执行结果:

volatile

现在有俩个线程,线程1判断flag是否等于0,如果是0就一直循环
线程2输入一个整数,修改flag
预期:t2把flag改为非0的一个整数,t1的循环也就结束了

代码演示

import java.util.Scanner;

class Mycounter{
    public  int flag = 0;
}
public class ThreadDemo14 {
    public static void main(String[] args) {
        Mycounter mycounter = new Mycounter();
        Thread t1 = new Thread(()->{
            while (mycounter.flag == 0) {
                //这个循环什么都不做,因此循环执行速度极快
            }
            System.out.println("t1线程 循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            mycounter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

 

运行结果

很明显运行结果和预期结果不符,出Bug了,
这种情况是内存可见性问题
这也是线程不安全的问题,一个线程读,一个线程改 

出现这种问题,我们可以手动给这个可能被别的线程修改的变量flag,加上volatile关键字
告诉编译器这个变量是易变的,每次都要重复读取这个变量的内容

运行结果

volatile不能修饰局部变量

内存可见性出现线程安全问题的原因是:一个线程读取变量 ,另一个线程可能会修改这个变量
局部变量不能再多线程之间,读取和修改
局部变量只能再当前方法内使用,出了方法,变量就没了
方法内部的变量在 栈 这样的空间上,每个线程都有自己的栈空间
即使是同一个方法,在多线程中被调用,这里的局部变量也会处在不同的栈空间中,本质还是不同的变量

wait 和 notify

wait和notify概念

线程最大的问题是,抢占式执行,随机调度

我们写代码不喜欢随机的,因为不确定的东西,可能会出现一些Bug
因此程序员发明了一些方法,来控制线程之间的顺序,虽然线程在系统内核里的调度是随机的
但是可以通过一些api让线程主动阻塞,主动放弃cpu(给别的线程让路)

比如,t1和t2俩个线程,希望t1先干活,干的差不多了,再让t2来干,
就可以让t2先wait(阻塞,主动放弃cpu),
等t1做的差不多的时候,再通过notify 通知t2,把t2唤醒,让t2来干活

wait和notify都是object类中的方法,也就是说任意对象都有wait和notify方法

wait进行阻塞

wait无参数版本就是 死等
wait带参数版本,指定了最大等待时间

某个线程调用wait方法,就会进入阻塞状态(无论通过哪个对象wait的),此时线程就处于WAITING状态

代码演示

public class ThreadDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Object object1 = new Object();
            System.out.println("wait 之前");
            object1.wait();
            System.out.println("wait 之后");      
    }
}

 运行结果

为什么会有这个异常?

wait方法的操作步骤

1.先释放锁

2.进行阻塞等待

3.收到通知后,重新尝试获取锁,并在获取到锁后,继续向下执行

这里的锁异常,就是因为,object还没有被加锁,就要释放锁,显然会出现锁异常状态

因此wait操作,要搭配 synchronized使用

public class ThreadDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Object object1 = new Object();
        synchronized (object1) {
            System.out.println("wait 之前");
            object1.wait();
            System.out.println("wait 之后");
        }
    }
}

 

写段代码展示notify的作用

现在有线程1调用了wait方法,线程2调用notify,唤醒线程1,让其继续执行

public class ThreadDemo16 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            //这个线程负责进行等待
            System.out.println("t1 wait 之前");
            synchronized (object){
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 wait 之后");
        });
        Thread t2 = new Thread(()->{
            System.out.println("t2 notify 之前");
            synchronized (object){
                //notify 务必获取到锁才能进行通知
                object.notify();
            }
            System.out.println("t2 notaify 之后");
        });
        t1.start();
        //sleep保证t1线程先执行,先进行wait阻塞等待
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

 

notifyAll

如果当前多个线程在等待 object对象
此时有一个线程 object.notify(),此时随机唤醒一个等待的线程(不知道具体哪个)
notifyAll,多个线程wait的时候,notifyAll所有线程都唤醒,这些线程再一起锁竞争

wait 和 sleep

wait 和 sleep都能等待一段时间,都能被提前唤醒
但它们有本质区别
wait这个方法用来等待,就是为了有朝一日能够被唤醒,notify唤醒 wait是不会有任何异常的,这时一个正常的逻辑
sleep这个方法,用来等待,是为了让线程休眠一段时间,不希望能够被唤醒,用interrupted唤醒sleep,会出现异常,这是一个出问题的逻辑

练习题

三个线程,分别只能打印A,B,C
写代码来保证三个线程,固定按照A,B,C的顺序来打印

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (locker1) {
                locker1.notify();
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1) {
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("B");
                synchronized (locker2) {
                    locker2.notify();
                }
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (locker2) {
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("C");
            }
        });
        t2.start();
        t3.start();
        Thread.sleep(1000);
        t1.start();
    }
}

 先创建locker1 和 locker2用来加锁的对象
再创建三个线程,t1执行打印A,t2打印B,t3打印C
在t3对象中,使用wait进行阻塞等待,加锁的对象为locker2
在t2对象中,也先使用wait进行阻塞等待,加锁对象为locker1,打印完B后,再用notify通知在locker2这个对象上等待的线程,
在t1对象中,先打印A,再用notify通知在locker1对象上等待的线程
再启动这三个线程,注意要保证,t1线程最后启动,否则t2线程还没有进行阻塞等待,t1就开始通知了

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

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

相关文章

LVGL学习笔记9 - 标签Label

目录 1. 显示字符串 1.1 lv_label_set_text 1.2 lv_label_set_text_fmt 1.3 lv_label_set_text_static 2. 设置长字符串模式 3. 改变颜色 3.1 改变背景颜色和对比度 3.2 设置字符串颜色 3.2.1 设置Style的字符串颜色 3.2.2 设置对象的字符串颜色 3.2.3 局部改色 显示…

钧瓷产业将占禹州GDP50%以上,产生千亿市值钧瓷生态型科技公司

这里的上市指沪深的主板&#xff0c;创业板和科创板&#xff0c;区域的挂牌不算。 这个数据是根据禹州钧瓷产业2022年实际税收&#xff0c;综合钧瓷产业报税幅度&#xff0c;钧瓷数据开放平台&#xff0c;钧瓷产业决策内参&#xff0c;钧瓷产业化&#xff0c; 数字化后的预期增…

【CUDA入门笔记】GPU存储结构模型(1)

GPU存储结构模型 1.CPU可以读写GPU设备中的Global Memory、Constant Memory以及Texture Memory内存储的内容&#xff1b;主机代码可以把数据传输到设备上&#xff0c;也可以从设备中读取数据&#xff1b; 2.GPU中的线程使用Register、Shared Memory、Local Memory、Global Mem…

信息时代,企业如何安全管理数据

随着企业信息化的发展&#xff0c;企业所产生的数据量也越来越多&#xff0c;企业数据的存储安全和传输安全管理工作则成为企业数据管理者的重中之重。但是对数据的保护要依靠一定的基础设施&#xff0c;目前&#xff0c;世界各国对数据保护的基础设施建设还是不够完善&#xf…

VSCode搭建ruby开发调试环境

安装rvm rvm是ruby版本管理工具&#xff0c;可以管理本地的ruby的版本 curl -sSL https://get.rvm.io | bash -s stable安装ruby 使用 rvm list known获取已知的ruby版本&#xff0c;这里安装3.0.0版本的ruby rvm install 3.0.0新建ruby文件 在VSCode中新建ruby文件main.r…

【强训】Day1

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、选择二、编程1. 组队竞赛2. 删除公共字符答案1. 选择2. 编程普通小孩也要热爱生活&#xff01; 一、选择 下列选项中属于面向对象编程主要特征的是&#xff08;&#xff09; A 继承 B 自顶向下 C 模块化 D 逐步…

【BP靶场portswigger-服务端4】操作系统命令注入-5个实验(全)

目录 一、操作系统命令注入 1、意义 2、有用的命令 3、注入操作系统命令的方式 4、防止操作系统命令注入攻击 二、执行任意命令 1、示例&#xff1a; 实验1&#xff1a;操作系统命令注入&#xff08;简单&#xff09; 三、盲操作系统命令注入漏洞 1、简述 2、示例 3…

Spring Cloud 2022.0.0正式发布:OpenFeign稳得很全面迈向GraalVM

本文已被https://yourbatman.cn收录&#xff1b;女娲Knife-Initializr工程可公开访问啦&#xff1b;程序员专用网盘https://wangpan.yourbatman.cn&#xff1b;技术专栏源代码大本营&#xff1a;https://github.com/yourbatman/tech-column-learning&#xff1b;公号后台回复“…

Vector在CANdb++中关于XCP和应用报文的定义

Vector DBC规则 前文讲解了dbc有关的属性定义与编辑,本文描述在开发过程中关于XCP和应用报文有关的规则说明,方便开发人员正确配置和代码生成所需的属性及其值。 关联文章: dbc的属性定义:dbc的属性定义 Vector DBC属性定义规则:Vector DBC属性定义规则 DBC编辑问题——…

我理解的proc伪文件系统

一.概念 提供可以动态操作Linux内核信息的接口&#xff0c;实现内核空间与用户空间进行数据交换的途径。 二.观察文件内容 //crtlaltt 快速打开Linux终端 //输入一下内容 cd /proc //进入proc文件夹 ls //观察proc文件夹下的内容 cd 2414 //任意打开一个带数字的文件…

【问题解决】解决xshell7会话窗口只能显示一个的问题

这恐怕会成为最短的一篇文章 问题复现 打开多个终端&#xff0c;最终只显示最后一个 如上图&#xff1a; 再打开一个192.168.1.42 &#xff0c;会覆盖掉1.41&#xff0c;终端上先显示最后打开的那个终端。 想要解决xshell7会话窗口只能显示一个的问题&#xff0c;我们只需要…

日志系统:一条SQL更新语句是如何执行的?

前面我们系统了解了一个查询语句的执行流程,并介绍了执行过程中涉及的处理模块。相信你还记得,一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。 那么,一条更新语句的执行流程又是怎样的呢? 之前你可能经常听 DBA 同事说,…

怎么判定自己的账号有没有被限流?短视频运营推广学习日记

短视频运营推广学习日记 今天开始记录自己的学习过程&#xff0c;今天的内容是&#xff0c;怎么知道自己的账号有没有被限流&#xff1f;对比了几个方法&#xff0c;还是我赢的内容比较正常&#xff0c;限流主要是两种方式&#xff1a;作品限流和账号限流 作品限流&#xff1…

开发第一天

首先下载Binary Editor: https://www.vcraft.jp/soft/bz.html 如图所示&#xff0c;点击下载&#xff1a; 下载后解压&#xff0c;在同一个盘下创建文件夹取名为OSASK,启动Bz.exe程序并输入&#xff1a; 需要复制很多的0,一直到168000这个地址&#xff0c;得到的文件命名为he…

(九) DockerFile

DockerFile一、概述二、DockerFile构建过程2.1、Dockerfile内容基础知识2.2、Docker执行Dockerfile的大致流程2.3、小总结三、DockerFile常用保留字指令四、实操案例4.1、要求Centos7镜像具备vimifconfigjdk84.2、编写4.3、Build构建镜像4.4、虚悬镜像一、概述 Dockerfile是用来…

C++18 -- 虚析构函数构成多态、纯虚函数、抽象类、虚继承

多态的条件&#xff1a; 1&#xff09;覆盖 2&#xff09;基类的指针或者引用 虚表的运行原理&#xff1a; 一、多态的特例 – 虚析构函数构成多态 类有指针作为数据成员&#xff0c;必须要写析构函数&#xff0c;如果当前类被继承了&#xff0c;则析构函数写成virtual&#…

四旋翼无人机学习第19节--allgero的板框导入,网表导入

文章目录1 板框导入2 网表导入3 颜色修改4 修改快捷键1 板框导入 1、板框可以在小马哥课程中获取哦。 课程地址:使用Cadence17.2 OrCAD Allegro绘制小马哥DragonFly四轴飞行器 2、下载得到文件&#xff0c;然后用CAD软件查看DXF文件&#xff0c;出现弹框点击是即可(文件只读)。…

科研小白如何做好科研(内附一些科研实用工具)

目录 前言 一、了解自己的研究方向 1、知其然并知其所以然 2、那如何做到呢&#xff1f; 二、拥有良好的科研素养 1、多读文献 2、夯实基础&#xff0c;搞清原理 3、不断学习&#xff0c;擅于总结 4、团队协作&#xff0c;勤沟通&#xff0c;多交流 三、掌握一些…

RHCEansible静态主机清单

首先要做好免密登录 RHCEansible虚拟机初始化配置&#xff0c;ansible配置和安装_无所不知的神奇海螺的博客-CSDN博客 添加主机组 [rootserver ~]# vim /etc/ansible/hosts 或者 测试 [rootserver ~]# ansible node1 -m command -a hostname --- 引号里的是想要受控主机执行的…

Dell inspiron 5488加装硬盘SSD

机械盘真心便宜&#xff0c; 当数据盘很合适。 ———— 我是装双系统&#xff0c; 希望速度快&#xff01; 我就装了一个SSD&#xff0c; STAT接口的&#xff0c; 和机械盘盒一样尺寸&#xff0c; 接口都是SATA&#xff0c; 我买的三星860EVO&#xff0c; 500G&…