【JavaEE多线程】从单例模式到线程池的深入探索

目录

    • 多线程案例
      • 单例模式
      • 阻塞队列
      • 定时器
      • 线程池
      • 总结-保证线程安全的思路
      • 对比线程和进程


多线程案例

单例模式

单例模式是一种设计模式

设计模式,就是程序员的棋谱,这里介绍了很多典型场景,以及典型场景的处理方式,按照设计模式写代码,代码写的肯定不会很差。

单例模式对应的场景,有些时候希望有的对象,在整个程序中只有一个实例(对象),只能new一次

写法:

  1. 饿汉模式(迫切):程序启动,类加载之后,立即创建出实例
class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
   }
}
  1. 懒汉模式(延时):类加载的时候不创建实例,则是在第一次使用实例的时候,再创建,否则,能不创建就不创建
class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}
  • 懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的

线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致

创建出多个实例.

一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改

instance 了)

加上 synchronized 可以改善这里的线程安全问题

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}
  • 懒汉模式-多线程版(改进)

以下代码在加锁的基础上, 做出了进一步改动:

  • 使用双重 if 判定, 降低锁竞争的频率.
  • 给 instance 加上了 volatile
class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {//第一个条件是为了判断是否要加锁,如果不是null就没必要加锁了
            synchronized (Singleton.class) {//是null就加锁
           if (instance == null) {//第二个条件是判断是否要创建对象,因为在多线程环境下可能有多个线程同时通过第一个条件检查
               instance = new Singleton();
               }
           }
       }
        return instance;
   }
}

即先判断是否要加锁,再决定是不是真的加锁

加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.

外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.

同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile .

当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作.

当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.

这里加上volatile还有一个作用:避免此处赋值操作的指令重排序

指令重排序:是编译器优化的一种手段,在原有执行逻辑不变的情况下,对代码执行顺序进行调整,是其执行效率变高

在单线程没事,但在多线程就可能出现问题了

比如我们上述代码在instance = new Singleton();这里我们可以分为3个步骤:

  1. 给对象创建出内存空间,得到内存地址
  2. 在空间上调用构造方法,对对象进行初始化
  3. 把内存地址,赋值个instance引用

这里发生指令重排序,步骤可能就变为132了,同样是在单线程没事,但多线程就不行了

比如线程1在这里执行132,在3执行完后2还没执行,出现了线程切换,线程2执行的时候instance!=null,就直接返回instance了,后续可能使用instance的属性方法之类的,因为还没初始化,就可能出现啥情况了。于是加入volatile就是避免这样的事情发生

这个**懒汉模式-多线程版(改进)**的关键就3点:

  1. 加锁
  2. 双重if
  3. volatile

阻塞队列

阻塞队列是什么

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则。

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.(削峰

比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.

这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.

  1. 阻塞队列也能使生产者和消费者之间解耦.

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.

擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).

标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

在这里插入图片描述

  1. 基于链表
  2. 基于堆,有优先级的
  3. 基于数组

Array这个版本速度更快,前提是知道有多少个元素。如果不知道有多少个元素就用Linked的

对于BlockingQueue来说,offer()pull()不带有阻塞功能,put()take()带有阻塞功能

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

阻塞队列实现

  • 通过 “循环队列” 的方式来实现.
  • 使用 synchronized 进行加锁控制.
  • put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
  • take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
class MyBlockingQueue{
    //使用一个String数组来保存数据,假设只存String
    private String[] items=new String[1000];
    volatile private int head=0;//队头
    volatile private int tail=0;//队尾
    //队列有效范围 [head,tail),当head和tail重合时,相当于空队列

    volatile private int size=0;//使用size表示元素个数

    private Object locker=new Object();

    //入队列
    public void put(String elem) throws InterruptedException {
        //加锁
        //此处的写法相当于直接把synchronized写到方法上了
        synchronized (locker){
            while(size>=items.length){//此处的while目的不是为了循环,而是借助循环巧妙的实现了wait被唤醒之后再次确认条件
                //队列满了
                //return;
                locker.wait();//这里的notify在出队列的方法中
            }
            items[tail++]=elem;
            if(tail>=items.length){
                tail=0;
            }//这整个判断语句的内容 <=> tail=(tail+1)%items.length,这个代码也能起到,当tail到达末尾就能回到开头
            size++;
            //使用这个notify来唤醒队列空的阻塞情况
            locker.notify();
        }
    }

    //出队列
    public String take() throws InterruptedException {
        //加锁
        synchronized (locker){
            while(size==0){//此处的while目的不是为了循环,而是借助循环巧妙的实现了wait被唤醒之后,再次确认条件
                //队列为空,暂时不能出队列
                //return null;
                locker.wait();//这里的notify在入队列的方法中
            }
            String elem=items[head++];
            if(head>=items.length){
                head=0;
            }
            size--;
            //使用这个notify来唤醒队列满的阻塞情况
            locker.notify();
            return elem;
        }
    }
}
//测试代码
public static void main(String[] args) throws InterruptedException {
    MyBlockingQueue queue=new MyBlockingQueue();
    //创建两个线程,表示生产者和消费者
    Thread t1=new Thread(()->{
        int count=0;
        while (true){
            try {
                queue.put(count+"");
                System.out.println("生产元素:"+count);
                count++;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    Thread t2=new Thread(()->{
        while (true){
            try {
                String count=queue.take();
                System.out.println("消费元素:"+count);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    t2.start();
}

上述代码是没问题的,但很容易写错在一个地方,就是在put()wait()的异常处理,这里在idea可以选择抛出异常也可以选择try-catch处理,但如果是try-catch处理,wait()interrupt唤醒后代码往下走进入catch,方法不会结束而是继续往下执行,就会强行添加元素然后覆盖元素,这是不应该的。而如果像上面一样抛出异常,出现异常后下面就不会继续执行了,就不会出现覆盖元素了。

定时器

定时器是什么

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

定时器是一种实际开发中非常常用的组件.

比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.

比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).

类似于这样的场景就需要用到定时器.

标准库中的定时器

  • 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

实现定时器

定时器的构成:

  • 一个带优先级的阻塞队列

为啥要带优先级呢?

因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.

  • 队列中的每个元素是一个 Task 对象.
  • Task 中带有一个时间属性, 队首元素就是即将
  • 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
  1. MyTimer 类提供的核心接口为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行.
public class MyTimer {
    public void schedule(Runnable command, long after) {
 	// TODO
    }
}
  1. MyTimerTask 类用于描述一个任务. 里面包含一个 Runnable 对象和一个 time(毫秒时间戳)

    这个对象需要放到 优先队列 中. 因此需要实现 Comparable 接口.

class MyTimerTask implements Comparable<MyTimerTask> {
    //任务啥时候执行
    private long time;
    //任务具体是啥
    private Runnable runnable;

    public MyTimerTask(Runnable runnable, long delay) {
        //delay是一个相对时间差
        //构造time要根据当前系统时间和delay进行构造
        time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //把时间小的优先级高,把它放入队首中
        // 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~
        // 随便写一个顺序, 然后实验一下就行了.
        return (int)(this.time-o.time);
        //return (int) (o.time - this.time);
    }
}
  1. MyTimer 实例中, 通过 PriorityBlockingQueue 来组织若干个 MyTimerTask 对象.

​ 通过 schedule 来往队列中插入一个个 MyTimerTask 对象.

class MyTimer {
    private Object locker=new Object();

    //使用优先级队列,来保存N个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //定时器的核心方法,把要执行的任务添加到队列中
    public void schedule(Runnable runnable, long delay) {
        synchronized(locker){
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            //每次来新的任务,都唤醒一下之前的扫描线程
            locker.notify();
        }
    }
}
  1. MyTimer 类中存在一个 t 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务

所谓 “能执行” 指的是该任务设定的时间已经到达了

  1. 引入一个 locker 对象, 借助该对象的 wait / notify 来解决 while (true) 的忙等问题.
class MyTimer {
    private Object locker=new Object();
    
    // ... 前面的代码不变

    //MyTimer中还需要构造一个“扫描线程”,一方面负责监控队首元素是否到点了,是否应该执行;
    // 一方面任务到点之后,就要调用Runnable的run方法来完成任务
    public MyTimer() {
        //扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized(locker){
                        while (queue.isEmpty()) {
                            //注意如果队列为空就不要取元素
                            //此处使用wait等待更合适,如果使用continue,就会使这个线程while循环运行的飞快
                            //也会陷入一个高频占用CPU的状态(忙等)
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            //假设当前时间14:01,任务时间14:00,就要应该执行
                            //需要执行任务
                            queue.poll();
                            task.getRunnable().run();
                        } else {
                            //让当前扫描线程休眠一下,按照时间差来进行休眠
                            //Thread.sleep(task.getTime() - curTime);可使用sleep就会有很多弊端,具体看7.28这天的板书
                            locker.wait(task.getTime()-curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

修改 MyTimer 的 schedule 方法, 每次有新任务到来的时候唤醒一下 t 线程. (因为新插入的任务可能是需要马上执行的).

class MyTimer {
    //定时器的核心方法,把要执行的任务添加到队列中
    public void schedule(Runnable runnable, long delay) {
        synchronized(locker){
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            //每次来新的任务,都唤醒一下之前的扫描线程
            locker.notify();
        }
    }
}

完整代码:

//创建一个类,描述定时器的一个任务
class MyTimerTask implements Comparable<MyTimerTask> {
    //任务啥时候执行
    private long time;
    //任务具体是啥
    private Runnable runnable;

    public MyTimerTask(Runnable runnable, long delay) {
        //delay是一个相对时间差
        //构造time要根据当前系统时间和delay进行构造
        time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    public long getTime() {
        return time;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //把时间小的优先级高,把它放入队首中
        // 怎么记忆, 这里是谁减去谁?? 不要记!! 记容易记错~~
        // 随便写一个顺序, 然后实验一下就行了.
        return (int)(this.time-o.time);
        //return (int) (o.time - this.time);
    }
}

//定时器类本体
class MyTimer {
    private Object locker=new Object();

    //使用优先级队列,来保存N个任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //定时器的核心方法,把要执行的任务添加到队列中
    public void schedule(Runnable runnable, long delay) {
        synchronized(locker){
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);
            //每次来新的任务,都唤醒一下之前的扫描线程
            locker.notify();
        }
    }

    //MyTimer中还需要构造一个“扫描线程”,一方面负责监控队首元素是否到点了,是否应该执行;
    // 一方面任务到点之后,就要调用Runnable的run方法来完成任务
    public MyTimer() {
        //扫描线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized(locker){
                        while (queue.isEmpty()) {
                            //注意如果队列为空就不要取元素
                            //此处使用wait等待更合适,如果使用continue,就会使这个线程while循环运行的飞快
                            //也会陷入一个高频占用CPU的状态(忙等)
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            //假设当前时间14:01,任务时间14:00,就要应该执行
                            //需要执行任务
                            queue.poll();
                            task.getRunnable().run();
                        } else {
                            //让当前扫描线程休眠一下,按照时间差来进行休眠
                            //Thread.sleep(task.getTime() - curTime);可使用sleep就会有很多弊端,具体看7.28这天的板书
                            locker.wait(task.getTime()-curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

线程池

线程池是什么

提前创建好一波线程,后续需要使用线程,就直接从线程池里拿即可

线程池最大的好处就是减少每次启动、销毁线程的损耗。

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
   }
});

Executors 创建线程池的几种方式

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装.

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定. (后面再介绍)

实现线程池

  • 核心操作为 submit, 将任务加入线程池中
  • 使用 t 描述一个工作线程. 使用 Runnable 描述一个任务.
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 t 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
  • 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.
class MyThreadPool{
    private BlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();

    //通过这个方法把任务添加到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    // n 表示线程池里有几个线程
    //创建一个固定数量的线程池
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t=new Thread(()->{
                while(true){
                    try {
                        //取出任务,并执行
                        Runnable runnable=queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

总结-保证线程安全的思路

  1. 使用没有共享资源的模型
  2. 适用共享资源只读,不写的模型
    1. 不需要写共享资源的模型
    2. 使用不可变对象
  3. 直面线程安全(重点)
    1. 保证原子性
    2. 保证顺序性
    3. 保证可见性

对比线程和进程

线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

进程与线程的区别

  1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  4. 线程的创建、切换及终止效率更高。

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

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

相关文章

火车头采集一键发布到Zblog

火车头采集发布到Zblog系统&#xff0c;主要操作步骤如下&#xff1a; 目录 1、Zblog火车头Web发布模块 2、内容发布参数映射&#xff0c;火车头发布到Zblog 3、简数一键发布到Zblog方法 1、Zblog火车头Web发布模块 自行编写Zblog火车头Web发布模块&#xff0c;一般要使用f…

Linux--进程间的通信--进程池

进程间的通信–匿名管道 进程池的概念 进程池是一种将多个进程组织起来以执行特定任务的机制。它由多个预先创建好的资源进程和一个管理进程组成。这些资源进程被管理进程负责分配和调度&#xff0c;用于处理任务。 当有新的任务提交时&#xff0c;管理进程会从进程池中取出一…

2024北京门窗展|2024北京门窗展会|2024北京门窗展览会

CWE中国&#xff08;北京&#xff09;国际系统门窗及幕墙博览会 CWE China&#xff08;Beijing&#xff09;International System Doors Windows and Curtain Walls Expo 2024年8月29-31日 北京&#xff0c;中国国际展览中心顺义馆 展会概况&#xff1a; 2024年CWE中国&…

【Unity】ScriptableObject 在游戏中的使用实例

ScriptableObject 在游戏中的使用实例 ScriptableObject 使用指南Unity 存储游戏数据的几种方法Unity ScriptableObject实例创建一个物品管理的ScriptableObject创建一个管理所有 ScriptableObject 的数据库&#xff08;ItemDBSO&#xff09; ScriptableObject 使用指南 Scrip…

回溯算法练习day.2

216.组合总和III 链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 找出所有相加之和为 n 的 k 个数的组合&#xff0c;且满足下列条件&#xff1a; 只使用数字1到9每个数字 最多使用一次 返回 所有可能的有效组合的列表 。该列表不能包含相…

基于STM32的RFID智能门锁系统

本文针对RFID技术&#xff0c;着重研究了基于单片机的智能门锁系统设计。首先&#xff0c;通过链接4*4按键模块与主控STM32&#xff0c;实现了多种模式&#xff0c;包括刷卡开锁、卡号权限管理、密码开锁、修改密码、显示实时时间等功能。其次&#xff0c;采用RC522模块与主控S…

【C语言基础】:预处理详解(二)

文章目录 一、宏和函数的对比二、#和##运算符2.1 #运算符2.2 ##运算符 三、#undef四、命令行定义五、条件编译六、头文件的包含1. 头文件包含的方式2. 嵌套文件包含 上期回顾&#xff1a; 【C语言基础】&#xff1a;预处理详解(一) 一、宏和函数的对比 宏通常被应有于执行简单…

Vue3---基础10(路由)

写一个最基本的路由导航 下载、创建、使用路由 下载路由 npm i vue-router 创建路由 先在 src 内去创建一个 router 文件夹 在文件夹内创建一个 index 文件 index.ts 内代码 // 创建一个路由器&#xff0c;并暴露出去 // 引入createRouter import { createRouter, createWeb…

CSS使用自己的字体

在项目的根目录下的static文件夹中放置字体文件。在项目中使用这个字体&#xff0c;需要2个步骤。 一. 你需要在全局样式文件中引入它。 假设你的全局样式文件是App.vue或者App.vue中引入的App.scss文件&#xff0c;你可以像这样引入字体文件&#xff1a; font-face {font-fa…

深度学习体系结构——CNN, RNN, GAN, Transformers, Encoder-Decoder Architectures算法原理与应用

1. 卷积神经网络 卷积神经网络&#xff08;CNN&#xff09;是一种特别适用于处理具有网格结构的数据&#xff0c;如图像和视频的人工神经网络。可以将其视作一个由多层过滤器构成的系统&#xff0c;这些过滤器能够处理图像并从中提取出有助于进行预测的有意义特征。 设想你手…

MySQL中的存储过程详解(上篇)

使用语言 MySQL 使用工具 Navicat Premium 16 代码能力快速提升小方法&#xff0c;看完代码自己敲一遍&#xff0c;十分有用 拖动表名到查询文件中就可以直接把名字拉进来中括号&#xff0c;就代表可写可不写 目录 1.认识存储过程 1.1 存储过程的作用 1.2 存储过程简介…

C#基础|数据类型、变量

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 01 数据类型 数据类型是为了方便存储数据的&#xff0c;为了将数据按照不同的分类存储&#xff0c;所以引入数据类型。这个在PLC中已经很熟悉了。 数据类型的作用&#xff1a;就是为了更好地管理内存&#xff0c;为…

顺序表 (头删 尾删 清空)

//头删 | 1 #include "head.h" | 1 #ifndef ww87 void head_del(p lp) | 2 int main(int argc, const char *argv[]) …

[C++][算法基础]求最小生成树(Kruskal)

给定一个 n 个点 m 条边的无向图&#xff0c;图中可能存在重边和自环&#xff0c;边权可能为负数。 求最小生成树的树边权重之和&#xff0c;如果最小生成树不存在则输出 impossible。 给定一张边带权的无向图 G(V,E)&#xff0c;其中 V 表示图中点的集合&#xff0c;E 表示图…

民航电子数据库:[E14024]事务内变更操作次数超过最大许可值10000,可通过系统参数max_trans_modify适当调整限制

目录 一、场景二、异常情况三、原因四、排查五、解决 一、场景 1、对接民航电子数据 2、执行delete语句时报错 二、异常情况 三、原因 通过报错信息就可以看出&#xff0c;是系统参数max_trans_modify配置导致 当删除的数据量 > max_trans_modify时&#xff0c;删除就会…

Visual studio项目默认“Header Files”、“Source Files”等过滤器消失后展开的方法。

使用Visual Studio进行项目开发创建默认工程的解决方案资源管理器里查看项目文件&#xff0c;所有的文件是按照其所属的类型自动归类&#xff0c;例如&#xff1a;.h头文件自动划归到Header Files文件夹&#xff0c;.cpp文件自动划归到Source Files文件夹下&#xff0c;如下图所…

关于AG32 MCU的一些奇思妙想

1、AG32VF103的网口是100M还是10M&#xff1f; RE: 都是100M的。 2、用FPGA能不能再仿出一个网口&#xff1f;有些产品用到两个网口。 理论上可以&#xff0c;但是要考虑&#xff0c;一个是cpld实现难度&#xff0c;一个是需要的逻辑单元。因为mac逻辑多&#xff0c;内置的2KL…

Python Flask Web 框架-API接口开发_4

一、1、安装 Falsk 当前用户安装 pip3 install --user Flask 确认安装成功&#xff1a; 进入python交互模式看下Flask的介绍和版本&#xff1a; $ python3>>> import flask >>> print(flask.__doc__)flask~~~~~A microframework based on Werkzeug. Its …

快速掌握Spring监控(Spring Boot admin)

监控 监控可视化监控平台Admin底层逻辑info 自定义端点 监控 监控的作用&#xff1a; 监控服务状态是否宕机监控服务运行指标&#xff08;内存&#xff0c;虚拟机&#xff0c;线程&#xff0c;请求等&#xff09;监控日志管理服务&#xff08;服务下线&#xff09; 监控的实…

linux进阶篇:使用Apache搭建文件服务器目录

Linux服务搭建篇&#xff1a;使用Apache搭建文件服务器目录 一、关于文件服务器 ​ 在一个项目中&#xff0c;如果想把公共软件或者资料共享给项目组成员&#xff0c;可以搭建一个简易的文件服务器来实现&#xff0c;只要是在局域网内的成员都可以通过浏览器或者wget命令来下…