JavaEE-经典多线程样例

文章目录

  • 单例模式
    • 设计模式初步引入
    • 为何存在单例模式
    • 饿汉式单例模式
    • 饿汉式缺陷以及是否线程安全
    • 懒汉式单例模式
    • 基础懒汉式缺陷以及是否线程安全
    • 懒汉式单例模式的改进
    • 完整代码(变量volatile)
  • 阻塞队列
    • 生产者消费者模型
    • 生产者消费者模型的案例以及优点
      • 请求与响应案例
      • 解耦合
      • 削峰填谷
    • 阻塞队列的内置API
    • 阻塞队列的模拟实现
      • 关于wait和while的搭配使用
      • 模拟实现

单例模式

设计模式初步引入

啥是设计模式?

  • 设计模式好⽐象棋中的 “棋谱”. 红⽅当头炮, ⿊⽅⻢来跳. 针对红⽅的⼀些⾛法, ⿊⽅应招的时候有⼀些固定的套路. 按照套路来⾛局势就不会吃亏.软件开发中也有很多常⻅的 “问题场景”. 针对这些问题场景, ⼤佬们总结出了⼀些固定的套路. 按照这个套路来实现代码, 也不会吃亏, 不针对某一种语言, 而是针对某种开发场景
  • 设计模式并不是只有23种, 因为之前有些大佬写了一本书叫设计模式,重点讨论了23种, 但事实上存在更多种的设计模式
  • 设计模式与框架的区别就是, 设计模式在开发中是软性要求(不一定遵守), 但是框架是硬性要求(一定要遵守)

简单点一句话总结

设计模式是前人根据一些开发场景给出的一些经验之谈, 所以设计模式并不针对某一种语言

为何存在单例模式

  • 单例模式能保证某个类在程序中只存在唯⼀⼀份实例, ⽽不会创建出多个实例.
    这⼀点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要⼀个,再
    比如如果一个类的创建需要加载的数据量非常的庞大(GB级别), 那我们不希望这
    个类频繁的创建销毁(开销很大), 我们可能只是希望创建一次就可以了

饿汉式单例模式

顾名思义, 这种方式实现的单例模式十分"饥渴", 不管使用不使用都会提前new一个对象

流程如下

  • 构造方法私有化
  • 定义一个静态的类对象用以返回
  • 提供一个公开的静态接口来获取唯一的对象

测试代码如下

/**
 * 下面定义一个类来测试饿汉式单例模式
 */
class HungrySingleton{
    // 提供一个静态的变量用来返回
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    // 构造方法私有化(在外部不可以构造对象)
    private HungrySingleton(){

    }

    // 提供一个获取实例的静态公开接口
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

public class DesignPatternTest {
    public static void main(String[] args) {
        // 对饿汉式单例的测试
        HungrySingleton instance1 = HungrySingleton.getInstance();
        HungrySingleton instance2 = HungrySingleton.getInstance();

        // 测试两者是不是一个对象
        System.out.println(instance1 == instance2);
    }
}

测试结果
在这里插入图片描述
很明显, 用这种方式创建的实例都是只有一份的…

饿汉式缺陷以及是否线程安全

首先饿汉式的单例模式缺陷是非常明显的

  • 饿汉式不管我们使用这个对象与否, 都会在类加载的时期(因为是静态对象)构建一个这样的对象, 但我们想要达成的效果是, 在我们不需要这种类的实例的时候, 我们不去进行构造对象的操作(变主动为被动)来减少内存等相关资源的开销

但是饿汉式单例一定是线程安全的

  • 构建对象的时期是类加载的时候, 后期不同线程对于这个实例的操作也仅仅是涉及到读操作, 不涉及修改操作, 所以当然是线程安全的, 不存在线程安全问题, 但是另一种实现的模式就不一定了

懒汉式单例模式

上面说了饿汉式单例模式的缺陷, 我们尝试使用懒汉式单例的方式去解决这个问题, 也就是仅仅在需要的时候进行new对象的操作

最基础的懒汉单例模式

构造的逻辑

  • 构造方法私有化
  • 提供一个静态的对象用来返回(暂时不new对象)
  • 提供一个公开访问的静态接口来返回唯一的对象

代码测试(最基础的版本)

/**
 * 下面定义一个类来测试懒汉式单例模式
 */
class LazySingleton{
    // 提供一个静态的变量用来返回
    private static LazySingleton lazySingleton = null;

    // 构造方法私有化(不可以在外部new对象)
    private LazySingleton(){

    }

    // 提供一个公开的获取实例的接口
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

public class DesignPatternTest {
    public static void main(String[] args) {
        // 对懒汉式单例的测试
        LazySingleton instance1 = LazySingleton.getInstance();
        LazySingleton instance2 = LazySingleton.getInstance();

        // 测试两者是不是一个对象
        System.out.println(instance1 == instance2);
    }
}

基础懒汉式缺陷以及是否线程安全

这个就和上面饿汉有较大的区别了, 虽然解决了在需要的时候进行new对象, 上面的基础版本的懒汉式在单线程的环境下肯定是没问题的, 但是在多线程的环境下就不好说了…看下面的分析

如果在多线程的环境下(我们假设有t1, t2)是下图的执行顺序
在这里插入图片描述
很明显这是一种类似串行的执行策略

但是还可能是下图的情况
在这里插入图片描述
t1线程判断完毕之后没有来得及进行new对象, t2线程紧接着进行了一次完整的new对象的过程, 此时t1线程又进行了一次new对象的过程, 很明显, 我们上面的情况进行了两次构造对象的过程, 同时拿到的对象也不一致

我们通过Thread.sleep()的方式进行延迟观察看是否会发生

/**
 * 下面定义一个类来测试懒汉式单例模式
 */
class LazySingleton{
    // 提供一个静态的变量用来返回
    private static LazySingleton lazySingleton = null;

    // 构造方法私有化(不可以在外部new对象)
    private LazySingleton(){

    }

    // 提供一个公开的获取实例的接口
    public static LazySingleton getInstance() throws InterruptedException {
        if(lazySingleton == null){
            Thread.sleep(1000);
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

public class DesignPatternTest {

    private static LazySingleton instance1 = null;

    private static LazySingleton instance2 = null;

    public static void main(String[] args) throws InterruptedException {
        // 创建两个线程获取实例
        Thread t1 = new Thread(() -> {
            try {
                instance1 = LazySingleton.getInstance();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                instance2 = LazySingleton.getInstance();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 开启两个线程
        t1.start();
        t2.start();

        // 睡眠等待一下
        Thread.sleep(2000);
        System.out.println(instance1 == instance2);
    }
}

在这里插入图片描述
很明显, 这样的懒汉式的代码是线程不安全的, 那要如何进行改进呢???

懒汉式单例模式的改进

之前我们说了, 要想保证线程是安全的, 有几种解决方式, 这里面我们就采取加锁, 因为其实
判断是不是null和对象应该是一个整体的原子性的操作

改进之后的代码

/**
 * 下面定义一个类来测试懒汉式单例模式(改进版)
 */
class LazySingleton{
    // 提供一个静态的变量用来返回
    private static LazySingleton lazySingleton = null;

    // 构造方法私有化(不可以在外部new对象)
    private LazySingleton(){

    }

    // 提供一个公开的获取实例的接口
    public static LazySingleton getInstance() {
        // 我们把if判断和new对象通过加锁打包为一个原子性的操作(这里使用类对象锁)
        synchronized (LazySingleton.class){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
        }
        return lazySingleton;
    }
}

public class DesignPatternTest {

    private static LazySingleton instance1 = null;

    private static LazySingleton instance2 = null;

    public static void main(String[] args) {
        // 在多线程中获取实例
        Thread t1 = new Thread(() -> {
            instance1 = LazySingleton.getInstance();
        });

        Thread t2 = new Thread(() -> {
            instance2 = LazySingleton.getInstance();
        });

        System.out.println(instance1 == instance2);
    }
}

这时候肯定是一个线程安全的代码了, 但是思考可不可以进一步改进呢???


当我们已经new个一次对象之后, 如果后续的线程想要获取这个对象, 那就仅仅是一个操作了, 根本不涉及对对象的修改, 但是我们每次都使用锁这样的机制就会造成阻塞, 也就会导致程序的效率下降, 所以我们对代码进行了下面的修改在外层再加一个if判断

改进的方法如下

// 提供一个公开的获取实例的接口
    public static LazySingleton getInstance() {
        // 我们把if判断和new对象通过加锁打包为一个原子性的操作(这里使用类对象锁)
        if (lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }

我们的两个if的含义

  • 第一个if: 判断对象是否创建完毕, 如果创建了, 只是一个读操作
  • 第二个if: 判断是不是需要new对象

可能初学多线程的时候, 看上述代码觉得很迷惑, 但其实这是因为之前我们写的程序都是单线程的情况, 单线程中执行流只有一个, 两次相同的if判断其实是没有必要的, 但是多线程的条件下, 是多个执行流, 相同的逻辑判断条件也可能产生不同的结果

完整代码(变量volatile)

关于变量是否会产生指令重排序和内存可见性问题, 我们直接加上volatile即可

/**
 * 下面定义一个类来测试懒汉式单例模式(完整改进版)
 */
class LazySingleton {
    // 提供一个静态的变量用来返回
    private volatile static LazySingleton lazySingleton = null;

    // 构造方法私有化(不可以在外部new对象)
    private LazySingleton() {

    }

    // 提供一个公开的获取实例的接口
    public static LazySingleton getInstance() {
        // 我们把if判断和new对象通过加锁打包为一个原子性的操作(这里使用类对象锁)
        if (lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}

阻塞队列

生产者消费者模型

关于生产者消费者模型, 其实是生活中抽象出来的一个模型案例, 我们举一个包饺子的例子来简单解释一下

  • 在包饺子的过程中, 存在一个擀饺子皮的人, 我们称之为生产者, 擀出来的饺子皮放到一个竹盘上, 这个竹盘相当于一个中间的媒介, 生产者生产的物质在上面与消费者进行交互, 而包饺子的人就是一个消费者, 从中间媒介中取出东西, 也就是消费的过程, 我们的中间的竹盘相当于一个缓冲, 如果包饺子的人包的快的话, 就需要等待做饺子皮的人, 如果做饺子皮的人做的快的话, 当竹盘放不下的时候就需要阻塞等待

  • 上面的情景抽象成生产者消费者模型, 擀饺子皮的人是生产者, 竹盖是阻塞队列, 包饺子的人是消费者

生产者消费者模型的案例以及优点

请求与响应案例

生产者消费者模型我们举一个"请求响应的案例"

在这里插入图片描述
图中我们也有解释, 越靠上游的消耗的资源越少
假设我们现在出现一个秒杀的请求, 上游可能还可以运行, 但是下游的服务器由于并发量过大就直接崩溃了

在这里插入图片描述
所以我们一般会对上面提供服务的逻辑进行改变
添加一个中间的结构(阻塞队列, 或者说消息队列)进行缓冲

在这里插入图片描述


在真实的开发场景当中, 阻塞队列甚至会单独的部署为一台服务器, 这种独立的服务器结构叫做消息队列, 可见其重要性


解耦合

生产者消费者模型的一个重要的优点就是让消费者和生产者解耦合

  • 根据上面的模型分析, 不管是生产者还是消费者都是面向阻塞队列来进行任务的执行的, 所以就降低了两者之间的耦合度, 将来想要修改这个模型的工作内容, 也只需要面向阻塞队列操作更改(其实相当于接口), 如果没有这种机制的话, 我们想要更改一个操作逻辑, 就需要同时修改消费者与生产者的代码结构…, 我们先前学习的接口其实就是一种解耦合的策略, 其核心就是减少耦合度, 便于对代码结构进行调整

削峰填谷

刚才我们的那个模型就说了, 如果消息请求量非常大的时候, 如果没有消息队列的存在, 就会对下游的服务器产生较大的影响, 甚至会导致服务器崩溃


下图是正常情况下消息队列的工作示意图, 添加的任务加入消息队列, 然后下游的服务器以一个相对稳定的效率从队列中取出来任务进行处理
在这里插入图片描述


下图是当任务量激增的时候, 虽然任务量激增, 但是依旧进入消息队列进行等待处理, 此时下游的服务器对任务的处理的效率基本不变, 所以可以保证处理的稳定性, 不至于让下游服务器崩溃, 因为这个消息一般都是一阵一阵的激增, 所以等到下一轮消息量减少的时候, 对先前消息队列的数据进行清理即可…
在这里插入图片描述

阻塞队列的内置API

下图是我们相关的阻塞队列的内置API继承逻辑

在这里插入图片描述


关于构造方法

ArrayBlockingQueue: 必须指定大小
LinkedBlockQueue: 可以指定也可以不指定


关于offerpollputtake的区别

首先是offerpoll

在这里插入图片描述
这两个方法也可以使阻塞队列产生阻塞的效果, 但是我们可以指定一个最大的等待时间
我们使用下面的代码测试

/**
 * 关于阻塞队列的相关测试
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 生成一个阻塞队列(指定队列的大小为100)
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(100);

        // 创建两个线程测试
        Thread producer = new Thread(() -> {
            for(int i = 0; i < 1000; i++){
                try {
                    blockingQueue.offer(i, 10L, TimeUnit.SECONDS);
                    System.out.println("生产了元素: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while(true){
                try {
                    // 进行休眠
                    Thread.sleep(1000 * 1);
                    int elem = blockingQueue.take();
                    System.out.println("消费了元素: " + elem);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

分析下这个程序的执行的逻辑

  • 在程序启动的很短的时间内, 由于阻塞队列的容量还有空余, 所以会大量的生产元素直到阻塞队列满了, 因为消费者线程是每一秒钟消耗一个元素, 所以存在等待时间, 我们上述代码设置的最大的等待时间是10s, 所以根本来不及等待到最大的时间点就可以进行取出元素…

puttake方法

  • 这组方法和上组方法的区别就是, 这个方法是当队列满或者队列空, 我们进行无限期的阻塞…, 直到队列中的元素不为空或者不为满就可以进行操作
/**
 * 关于阻塞队列的相关测试
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 生成一个阻塞队列(指定队列的大小为100)
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(100);

        // 创建两个线程测试
        Thread producer = new Thread(() -> {
            for(int i = 0; i < 1000; i++){
                try {
                    blockingQueue.put(i);
                    System.out.println("生产了元素: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while(true){
                try {
                    // 进行休眠
                    Thread.sleep(1000 * 1);
                    int elem = blockingQueue.take();
                    System.out.println("消费了元素: " + elem);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

最后的执行结果如下

在这里插入图片描述
在短时间之内进行大量的生产之后开始隔一秒拿出一个元素, 生产一个元素

阻塞队列的模拟实现

关于wait和while的搭配使用

在这里插入图片描述
上面是我们的JDK帮助文档wait使用的建议(其实就是源码), 我们官方文档中提倡wait的使用建议和while循环搭配, 而不是和if搭配…原因下面解释

模拟实现

其实就是一个循环队列, 在put方法加入元素的时候如果队列是满的就进行阻塞, 在take方法拿出元素的时候如果队列是空的也进行阻塞(使用wait), 然后put方法添加了一个元素之后, 使用notify方法对take正在阻塞的线程进行唤醒(随机唤醒), 下面是实现代码


/**
 * 自己实现一个阻塞队列
 * 1. 使用循环数组
 * 2. 使用wait-notify进行线程见的通信
 * 3. 关于wait的使用的while机制
 */
public class MyBlockingQueue {

    // 我们定义这个阻塞队列中的元素是int类型
    private int capacity = 0;

    private int[] queue = null;

    // 构造方法
    public MyBlockingQueue(int capacity) {
        this.capacity = capacity;
        queue = new int[capacity];
    }

    // 定义队首尾的指针以及元素个数
    private int first = 0;

    private int last = 0;

    private int size = 0;

    // 判断队列是否为空
    private boolean isEmpty() {
        return size == 0;
    }

    // 判断队列是否是满的
    private boolean isFull() {
        return size == capacity;
    }

    // put操作
    public void put(int val) throws InterruptedException {
        while (isFull()) {
            // 此时进入阻塞等待
            synchronized (this) {
                this.wait();
            }
        }
        queue[last] = val;
        last = (last + 1) % capacity;
        size++;
        // 随机唤醒一个线程
        synchronized (this) {
            this.notify();
        }
    }

    // take操作
    public int take() throws InterruptedException {
        while (isEmpty()) {
            // 此时进入阻塞等待
            synchronized (this) {
                this.wait();
            }
        }
        int res = queue[first];
        first = (first + 1) % capacity;
        size--;
        // 随机唤醒一个线程
        synchronized (this) {
            this.notify();
        }
        return res;
    }
}

class Test {
    public static void main(String[] args) {
        // 对实现的队列进行测试
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);

        // 创建生产者线程进行测试
        Thread producer = new Thread(() -> {
            for(int i = 0; i < 1000; i++){
                try {
                    myBlockingQueue.put(i);
                    System.out.println("生产了元素: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 创建消费者线程进行测试
        Thread consumer = new Thread(() -> {
            for(int i = 0; i < 1000; i++){
                try {
                    Thread.sleep(1000);
                    int getElem = myBlockingQueue.take();
                    System.out.println("消费了元素: " + getElem);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 启动两个线程
        producer.start();
        consumer.start();
    }
}

在这里插入图片描述
瞬间产出100个元素之后进行阻塞, 产出一个消耗一个…


为什么要使用while代替if

 // put操作
    public void put(int val) throws InterruptedException {
        if(isFull()) {
            // 此时进入阻塞等待
            synchronized (this) {
                this.wait();
            }
        }
        queue[last] = val;
        last = (last + 1) % capacity;
        size++;
        // 随机唤醒一个线程
        synchronized (this) {
            this.notify();
        }
    }

 // put操作
    public void put(int val) throws InterruptedException {
        while (isFull()) {
            // 此时进入阻塞等待
            synchronized (this) {
                this.wait();
            }
        }
        queue[last] = val;
        last = (last + 1) % capacity;
        size++;
        // 随机唤醒一个线程
        synchronized (this) {
            this.notify();
        }
    }

我们分析一下两个相同的操作, 使用whileif的区别

在这里插入图片描述
这一张图片揭示了为什么使用wait搭配while使用更加合理

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

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

相关文章

Web3的技术栈详解:解读区块链、智能合约与分布式存储

随着数字时代的不断发展&#xff0c;Web3作为下一代互联网的核心理念逐渐走进了大众视野。它承载着去中心化、用户主权以及更高效、更安全的网络环境的期望。Web3不再是由少数中心化机构主导的网络&#xff0c;而是通过一系列核心技术的支撑&#xff0c;给每个用户赋予了更多的…

贪心算法实例-问题分析(C++)

贪心算法实例-问题分析 饼干分配问题 有一群孩子和一堆饼干&#xff0c;每个小孩都有一个饥饿度&#xff0c;每个饼干都有一个能量值&#xff0c;当饼干的能量值大于等于小孩的饥饿度时&#xff0c;小孩可以吃饱&#xff0c;求解最多有多少个孩子可以吃饱?(注:每个小孩只能吃…

虚幻引擎---材质篇

一、基础知识 虚幻引擎中的材质&#xff08;Materials&#xff09; 定义了场景中对象的表面属性&#xff0c;包括颜色、金属度、粗糙度、透明度等等&#xff1b;可以在材质编辑器中可视化地创建和编辑材质&#xff1b;虚幻引擎的渲染管线的着色器是用高级着色语言&#xff08;…

Python从入门到入狱

Python是从入门到入狱&#xff1f;这个充满调侃意味的说法在程序员圈子里流传甚广。表面看&#xff0c;它似乎是在嘲笑这门语言从简单易学到深陷麻烦的巨大反差&#xff0c;实际上却隐藏着很多值得深思的问题。要解读这个话题&#xff0c;得从Python的特点、使用场景以及潜在风…

使用PaddlePaddle实现线性回归模型

目录 ​编辑 引言 PaddlePaddle简介 线性回归模型的构建 1. 准备数据 2. 定义模型 3. 准备数据加载器 4. 定义损失函数和优化器 5. 训练模型 6. 评估模型 7. 预测 结论 引言 线性回归是统计学和机器学习中一个经典的算法&#xff0c;用于预测一个因变量&#xff0…

华为NPU服务器昇腾Ascend 910B2部署通义千问Qwen2.5——基于mindie镜像一路试错版(三)

文章目录 前言纯模型推理启动服务后面干什么?这可咋整啊?愁死了!总结前言 这是咱这个系列的第三个文章了。 毕竟,这是我好几天摸索出的经验,能帮助各位在几个小时内领会,我觉得也算是我的功劳一件了。 所以,一是希望大家耐心看下去,耐心操作下去;而是恳请各位多多关…

BERT模型的输出格式探究以及提取出BERT 模型的CLS表示,last_hidden_state[:, 0, :]用于提取每个句子的CLS向量表示

说在前面 最近使用自己的数据集对bert-base-uncased进行了二次预训练&#xff0c;只使用了MLM任务&#xff0c;发现在加载训练好的模型进行输出CLS表示用于下游任务时&#xff0c;同一个句子的输出CLS表示都不一样&#xff0c;并且控制台输出以下警告信息。说是没有这些权重。…

【Linux操作系统】多线程控制(创建,等待,终止、分离)

目录 一、线程与轻量级进程的关系二、进程创建1.线程创建线程创建函数&#xff08;pthread&#xff09;查看和理解线程id主线程与其他线程之间的关系 三、线程等待&#xff08;回收&#xff09;四、线程退出线程退出情况线程退出方法 五、线程分离线程的优点线程的缺点 一、线程…

Android ConstraintLayout 约束布局的使用手册

目录 前言 一、ConstraintLayout基本介绍 二、ConstraintLayout使用步骤 1、引入库 2、基本使用&#xff0c;实现按钮居中。相对于父布局的约束。 3、A Button 居中展示&#xff0c;B Button展示在A Button正下方&#xff08;距离A 46dp&#xff09;。相对于兄弟控件的约束…

【论文复现】隐式神经网络实现低光照图像增强

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀ 隐式神经网络实现低光照图像增强 引言那么目前低光照图像增强还面临哪些挑战呢&#xff1f; 挑战1. 不可预测的亮度降低和噪声挑战2.度量友好…

【机器学习】机器学习的基本分类-监督学习-决策树-C4.5 算法

C4.5 是由 Ross Quinlan 提出的决策树算法&#xff0c;是对 ID3 算法的改进版本。它在 ID3 的基础上&#xff0c;解决了以下问题&#xff1a; 处理连续型数据&#xff1a;支持连续型特征&#xff0c;能够通过划分点将连续特征离散化。处理缺失值&#xff1a;能够在特征值缺失的…

Spring和SpringBoot的关系和区别?

大家好&#xff0c;我是锋哥。今天分享关于【Spring和SpringBoot的关系和区别&#xff1f;】面试题。希望对大家有帮助&#xff1b; Spring和SpringBoot的关系和区别&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring和Spring Boot是两种相关但有所…

Scrapy 中的配置笔记

概述 scrapy在命令启动之前&#xff0c;先设置好了各种配置文件。其中包括系统自带的默认配置文件&#xff0c;还有用户自定义的settings.py。其中还有一个日常开发中不怎么用的scrapy.cfg文件&#xff0c;这个文件是用来告诉scrapy用户自定义的settings.py文件在哪里的 关键…

代码随想录算法训练营day49|动态规划part11

最长公共子序列 这个与上篇笔记最大的不同就是子序列里的数可以不相邻,那么只需加入一个dp[i][j]的上和左的更新方向即可 class Solution { public:int longestCommonSubsequence(string text1, string text2) {vector<vector<int>> dp(text1.size()1,vector<…

Python知识分享第十九天-网络编程

网络编程 概述用来实现 网络互联 不同计算机上运行的程序间可以进行数据交互也叫Socket编程 套接字编程 三要素IP地址概述设备在网络中的唯一标识分类IPV4城域网13广域网22局域网31IPV6八字节 十六进制相关dos命令查看ipwindows: ipconfigmac和linux: ifconfig测试网络ping 域…

CAN接口设计

CAN总线的拓扑结构 CAN总线的拓扑结构有点像485总线,都是差分的传输方式,总线上都可以支持多个设备,端接匹配电阻都是120Ω。 485和CAN通信方面最大的区别:网络特性。485是一主多从的通讯方式,CAN是多主通讯,多个设备都可以做主机。那多个设备都相要控制总线呢?…

Latex转word(docx)或者说PDF转word 一个相对靠谱的方式

0. 前言 投文章过程中总会有各种各样的要求&#xff0c;其中提供word格式的手稿往往是令我头疼的一件事。尤其在多公式的文章中&#xff0c;其中公式转换是一个头疼的地方&#xff0c;还有很多图表&#xff0c;格式等等&#xff0c;想想就让人头疼欲裂。实践中摸索出一条相对靠…

数据结构——单调队列

这篇博客我们来讨论一下单调队列的问题&#xff0c;其实和之前学的单调栈都是一种上通过改变操作来解决问题的一种数据结构 我们先来回忆一下单调栈的内容&#xff0c;这样方便将其和单调队列做区分 单调栈&#xff1a;(单调性从栈底到栈顶&#xff09; 1.单调栈是一种栈数据…

解决 Maven 部署中的 Artifact 覆盖问题:实战经验分享20241204

&#x1f6e0;️ 解决 Maven 部署中的 Artifact 覆盖问题&#xff1a;实战经验分享 &#x1f4cc; 引言 在软件开发过程中&#xff0c;持续集成和持续部署&#xff08;CI/CD&#xff09;是提高开发效率和代码质量的关键手段。Hudson 和 Maven 是两种广泛使用的工具&#xff0…

[go-redis]客户端的创建与配置说明

创建redis client 使用go-redis库进行创建redis客户端比较简单&#xff0c;只需要调用redis.NewClient接口创建一个客户端 redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379",Password: "",DB: 0, })NewClient接口只接收一个参数red…