单例模式与多线程

目录

前言

正文

1.立即加载/饿汉模式 

2.延迟加载/懒汉模式 

1.延迟加载/懒汉模式解析 

2.延迟加载/懒汉模式的缺点 

3.延迟加载/懒汉模式的解决方案 

(1)声明 synchronized 关键字

(2)尝试同步代码块

(3)针对某些重要的代码进行单独的同步

(4)使用 DCL 双检查锁机制

(5)双检查锁 DCL 使用 volatile 的必要性 

3.使用静态内置类实现单例模式 

4.序列化和反序列化的单例模式实现 

5.使用 static 静态代码块实现单例模式 

6.使用 enum 枚举类型实现单例模式 

7.完善使用 enum 枚举类实现单例模式 

总结


 

前言

在单例模式与多线程技术相结合的过程中,我们能发现许多以前从未考虑过的问题。这些不良的程序设计如果应用在商业项目中将会带来非常大的麻烦。线程与某些技术相结合时,我们需要考虑的事情会更多。总的来说,在本节我们只需要考虑一件事,那就是:如何使单例模式与多线程结合时是安全的、正确的。


正文

在标准的23个设计模式中,单例模式在应用中是比较常见的。但多数常规的该模式教学并没有结合多线程技术进行介绍,这就造成在使用结合多线程的单例模式时会出现一些意外。这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。 

1.立即加载/饿汉模式 

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕。常见的实现办法就是 new 实例化。从中文的语境上来看,就是 “着急” “急迫” 的含义,所以也被称为 “饿汉模式”。 

实现代码:

public class MyObject {
    //立即加载方式 == 饿汉模式
    private static MyObject myObject = new MyObject();

    private MyObject() {
    }
    public static MyObject getInstance(){
        return myObject;
    }

}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

程序运行结果如图:

控制台打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例模式。

此代码版本是立即加载模式,缺点是不能有其他实例变量,因为 getInstance() 方法没有同步,所以有可能出现非线程安全问题,比如出现如下代码:

public class MyObject {
    //立即加载方式 == 饿汉模式
    private static MyObject myObject = new MyObject();

    private MyObject() {
    }
    private static String username;
    private static String password;
    public static MyObject getInstance(){
        username = "从不同的服务器取出值(有可能不一样),并赋值";
        password = "从不同的服务器取出值(有可能不一样),并赋值";
        //上面的赋值并没有被同步,所以极易出现非线程安全问题,导致变量值被覆盖。
        return myObject;
    }

}

2.延迟加载/懒汉模式 

什么是延迟加载》延迟加载就是调用 get() 方法时,实例才被工厂创建。常见的实现办法就是在 get() 方法中进行 new 实例化。 延迟加载从中文的预警来看,是 “缓慢” “不急迫” 的含义,所以也被称为 “懒汉模式”。 

1.延迟加载/懒汉模式解析 

实现代码:

public class MyObject {
    private static MyObject myObject;

    public MyObject() {
    }
    public static MyObject getInstance(){
        //延迟加载
        if (myObject != null){
        }else {
            myObject = new MyObject();
        }
        return myObject;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}


public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        MyThread t2 = new MyThread();
        t2.start();
    }
}

程序运行结果如图:

此代码虽然取得一个对象的实例,但在多线程环境中会出现取出多个实例的情况,与单例模式的初衷是违背的。 

2.延迟加载/懒汉模式的缺点 

前面两个实验虽然使用了 "立即加载" 和 "延迟加载" 实现了单例模式,但在多线程环境中,"延迟加载" 示例中的代码完全是错误的,跟本不能保持单例的状态。下面来看如何在多线程环境中结合错误的单例模式创建出多个实例的。 

public class MyObject {
    private static MyObject myObject;
    private MyObject(){
    }
    public static MyObject getInstance(){
        try {
            if (myObject != null){
            }else {
                //模拟创建对象之前做一些准备工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

程序运行结果如图所示:

控制台打印出 3 种 hashCode,说明创建出 3 个对象,并不是单例的,这就是 "错误的单例模式",如何解决呢?下面看一下解决方案。

3.延迟加载/懒汉模式的解决方案 

(1)声明 synchronized 关键字

既然多个线程可以同时进入 getInstance() 方法,我们只需要对 getInstance() 方法声明 synchronized 关键字即可。

public class MyObject {
    private static MyObject myObject;
    private MyObject(){
    }
    //设置同步方法效率太低
    //整个方法被上锁
    synchronized public static MyObject getInstance(){
        try {
            if (myObject != null){
            }else {
                //模拟创建对象之前做一些准备工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果如图:

此方法在加入同步 synchronized 关键字后得到相同实例的对象,但运行效率非常低。下一个线程想要取得对象,必须等上一个线程释放完锁之后,才可以执行。那换成同步代码块可以解决吗?

(2)尝试同步代码块

创建测试用例

public class MyObject {
    private static MyObject myObject;
    private MyObject(){
    }
    //设置同步方法效率太低
    //整个方法被上锁
    public static MyObject getInstance(){
        try {
            //与同步方法等同
            //效率一样很低,并不能减少锁的粒度
            // (已经是最小的范围了,必须要在if判断前加锁,不然进入else还是会创建多个对象)
            // 全部代码同步运行
            synchronized (MyObject.class) {
                if (myObject != null){
                }else {
                    //模拟创建对象之前做一些准备工作
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
        //此版本代码虽然是正确的
        //但全部代码都是同步的,这样做也有损效率
    }
}

运行结果如图 :

此方法在加入同步 synchronized 语句块后得到相同实例的对象,但运行效率也非常低,和synchronized 同步方法一样是同步运行的。下面继续更改代码,尝试解决这个问题。

(3)针对某些重要的代码进行单独的同步

同步代码块可以仅针对某些重要的代码进行单独的同步,这可以大幅度提升效率 。

创建代码如下:

public class MyObject {
    private static MyObject myObject;
    private MyObject(){
    }
  
    public static MyObject getInstance(){
        try {
          
                if (myObject != null){
                }else {
                    //模拟创建对象之前做一些准备工作
                    Thread.sleep(3000);
                    //使用部分代码被上锁
                    //但还是有非线程安全问题
                    //多次创建 MyObject 类的对象,结果并不是单例
                    synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

程序结果如图:

 此方法使同步 synchronized 语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行的效率的确得到了提升 ,但遇到多线程情况还是无法得到同一个实例对象。到底如何解决懒汉模式下的多线程情况呢?

(4)使用 DCL 双检查锁机制

下面使用 DCL 双重检查锁机制来实现多线程环境中的延迟加载单例模式。 

  1. 第一次检查(无锁操作): 检查实例是否已经被创建,如果已经被创建,则直接返回实例,可以减少不必要的锁竞争(即每次都进入synchronized中);
  2. 第二次检查(锁内操作): 如果第一次检查发现实例尚未创建,代码会进入一个同步块,但在创建实例之前,会再次检查实例是否被创建。这是必要的,因为在当前线程进入同步块之前,可能有另一个线程已经创建了实例。
  • 性能优化:通过减少同步的使用,DCL减少了不必要的性能开销,因为实例一旦被创建后,就不再需要同步。
  • 线程安全:通过同步块确保在实例未初始化时,只有一个线程能创建单例实例,保持了单例的线程安全约定。
  • 资源利用最优化:由于同步只在实际需要时才会发生,因此在资源利用上比始终同步要有效得多。

要注意的是,在Java中使用DCL的时候还需要考虑Java内存模型的因素。在多线程环境下,为了确保DCL正确地工作,单例对象的引用需要被声明为 volatile,这样可以防止指令重排序可能导致的DCL失效问题。

public class MyObject {
    private volatile static MyObject myObject;
    private MyObject(){
    }
    //设置同步方法效率太低
    //整个方法被上锁
    public static MyObject getInstance(){
        try {
                if (myObject != null){
                }else {
                    //模拟创建对象之前做一些准备工作
                    Thread.sleep(3000);
                    synchronized (MyObject.class) {
                        if (myObject==null){
                            myObject = new MyObject();
                        }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
    //此版本的代码称为:
    //双重检查 Double-Check
}

使用 volatile 修饰变量 myObject ,使该变量在多个线程间可见,另外禁止 myObect = new MyObject() 代码重排序。myObject = new MyObject(); 代码包含 3 个步骤。

  1. memory = allocate(); //分配对象的内存空间
  2. ctorInstance(memory); //初始化对象
  3. myObject = memory;    //设置 instance 指向刚分配的内存地址

JIT 编译器有可能将这三个步骤重排序成。

  1. memory = allocate(); //分配对象的内存空间
  2. myObject = memory;    //设置 instance 指向刚分配的内存地址
  3. ctorInstance(memory); //初始化对象

这时,构造方法虽然还没有执行,但 myObject 对象已具有内存地址,即值不是 null。当访问 myObject 对象中的值时,是当前声明数据类型的默认值,此知识点在后面的章节中有讲解。

创建线程类的代码如下:

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

创建运行类的代码如下:

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果如图所示:

可见 DCL 双检查锁成功解决了懒汉模式下的多线程问题。DCL 也是大多数线程结合单例模式使用的解决方案 。

(5)双检查锁 DCL 使用 volatile 的必要性 

 前面介绍了 myObject = new MyObject() 代码中的 3 个步骤会发生重排序,导致取得实例变量的值不是构造方法初始化后的值。下面开始验证。

创建测试用例 

package org.example.singleton;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class dcl_and_volatile {
    static class OneInstanceService {
        public int i_am_has_state = 0;
        private volatile static OneInstanceService test;

        public OneInstanceService() {
            this.i_am_has_state = new Random().nextInt(200) + 1;
        }

        public static OneInstanceService getTest1() {
            if (test == null) {
                synchronized (OneInstanceService.class) {
                    if (test == null) {
                        test = new OneInstanceService();
                    }
                }
            }
            return test;
        }

        public static void reset() {
            test = null;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            //允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
            CountDownLatch latch = new CountDownLatch(1);
            CountDownLatch end = new CountDownLatch(100);
            for (int i = 0; i < 100; i++) {
                Thread t1 = new Thread() {
                    @Override
                    public void run() {
                        try {
                            //导致当前线程等到锁存器计数到零,除非线程是 interrupted 。
                            //创建 100 个线程在这里等待
                            latch.await();
                            OneInstanceService one = OneInstanceService.getTest1();
                            if (one.i_am_has_state == 0) {
                                System.out.println("one.i_am_has_state == 0 进程结束");
                                System.exit(0);
                            }
                            //减少锁存器的计数,如果计数达到零,释放所有等待的线程。
                            end.countDown();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                t1.start();
            }
            //循环完毕,创建结束,减掉计数1,线程被唤醒开始执行
            latch.countDown();
            //等待计数为0,也就是100个线程执行完成
            end.await();
            //重置
            OneInstanceService.reset();
        }
    }
}

程序在运行时添加 VM 参数 -server 会更容易获得预期的结果,运行后控制台结果如图:

说明 myObject = new myObject() 确实发生了重排序。

更改代码:

static class OneInstanceService{
        public int i_am_has_state = 0;
        private volatile static OneInstanceService test;

        public OneInstanceService() {
            this.i_am_has_state = new Random().nextInt(200)+1;
        }
        public static OneInstanceService getTest1(){
            if (test == null){
                synchronized (OneInstanceService.class){
                    if (test == null){
                        test = new OneInstanceService();
                    }
                }
            }
            return test;
        }
        public static void reset(){
            test = null;
        }
    }

程序运行后不再打印任何信息,说明禁止重排序后,实例变量 i_am_has_state 永远不是 0 了。也就是说,步骤 A 开辟空间 B 来执行构造方法 C,在赋值代码中插入屏障 ,防止 B 跑到 C 的后面,这样执行顺序永远是 ABC ,而且使用 volatile 还保证了变量的值在多个线程间可见。

3.使用静态内置类实现单例模式 

DCL 可以解决多线程单例模式的非线程安全问题。我们还可以使用其他办法达到同样的效果。 

创建新的测试用例:

public class MyObject {
    private static class MyobjectHandler {
        private static MyObject myObject = new MyObject();
    }

    private MyObject() {
    }

    public static MyObject getInstance() {
        return MyobjectHandler.myObject;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

程序运行后的效果如图:

 

4.序列化和反序列化的单例模式实现 

如果将单例对象进行序列化,使用默认的反序列行为取出对象是多例的。 

创建测试用例:

//实体类代码
public class Userinfo {
}

//创建类 MyObject.java
import java.io.ObjectStreamException;
import java.io.Serializable;

public class MyObject implements Serializable {
    private static final long serialVersionUID = 888L;
    public static Userinfo userinfo = new Userinfo();
    private static MyObject myObject = new MyObject();

    private MyObject() {
    }

    public static MyObject getInstance() {
        return myObject;
    }

    /*protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了 readResolve方法!");
        return MyObject.myObject;
    }*/
}

方法  protected Object readResolve() 的作用是反序列化时不创建新的 MyObject 对象,而是复用原有的 MyObject 对象。

创建业务类代码:

import java.io.*;

public class SaveAndRead {
    public static void main(String[] args) {
        try {
            MyObject myObject = MyObject.getInstance();
            System.out.println("序列化 -myObject="+myObject.hashCode()+" userinfo="+myObject.userinfo.hashCode());
            FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));
            ObjectOutput oosRef = new ObjectOutputStream(fosRef);
            oosRef.writeObject(myObject);
            oosRef.close();
            fosRef.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt"));
            ObjectInput iosRef = new ObjectInputStream(fisRef);
            MyObject myObject = (MyObject) iosRef.readObject();
            iosRef.close();
            fisRef.close();
            System.out.println("    序列化 -myObject="+myObject.hashCode()+" userinfo="+myObject.userinfo.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

程序运行结果如图:

 

从打印结果可以分析出,在反序列化时创建新的 MyObject 对象,但 Userinfo 对象得到复用,因为 hashcode 是同一个 1163157884 。为了实现 MyObject 在内存中一直呈单例效果,我们可以在反序列化中使用 readResolve() 方法,对原有的 MyObject 对象进行复用:

protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了 readResolve方法!");
        return MyObject.myObject;
    }

程序运行结果如图:

 

方法 protected Object readResolve() 的作用是在反序列化时不创建新的 MyObject 对象,而是复用 JVM 内存中原有的 MyObject 单例对象,即 Userinfo 对象被复用,这就实现了对 MyObject 序列化与反序列化时保持单例性。

注意:如果将序列化和反序列化操作分别放入两个 class,反序列化时会产生新的 MyObject 对象。放在 2 个 class 类中分别执行其实相当于创建了 2 个 JVM 虚拟机,每个虚拟机里有 1 个 MyObject 对象。我们想要实现的是在 1 个 JVM 虚拟机中进行序列化与反序列化时保持 MyObject 单例性,而不是创建 2 个 JVM 虚拟机。  

补充: 在Java中,对象的序列化是将对象的状态(state)序列化为字节流,而不是重新创建对象。在序列化过程中,对象的引用会被保存下来,而不是对象本身。反序列化时,根据保存的引用创建新的对象,并将序列化的状态恢复到新对象中。  在这段代码中,userinfo对象作为myObject对象的成员变量,被序列化时也一起序列化了。反序列化时,根据之前的引用创建新的myObject对象,并将序列化的userinfo对象的状态恢复到新对象中。因此,userinfo对象并没有被重新创建,而是在序列化和反序列化过程中被复原了状态。 

5.使用 static 静态代码块实现单例模式 

静态代码块中的代码在使用类的时候就已经执行,所以我们可以应用静态代码块的这个特性实现单例模式。 

public class MyObject {
    private static MyObject instance = null;
    private MyObject(){
    }
    static {
        instance = new MyObject();
    }
    public static MyObject getInstance(){
        return instance;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果如图:

 

6.使用 enum 枚举类型实现单例模式 

枚举 enum 和静态代码块的特性相似。在使用枚举类时,构造方法会被自动调用。我们也可以应用这个特性实现单例模式。 

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public enum MyObject {
    connectionFactory;
    private Connection connection;

    private MyObject() {
        try {
            System.out.println("调用了 MyObject 的构造器");
            String url = "jdbc:mysql://localhost:3306/spring_boot";
            String username = "root";
            String password = "123456";
            String driverName = "com.mysql.cj.jdbc.Driver";
            Class.forName(driverName);
            connection  = DriverManager.getConnection(url,username,password);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public Connection getConnection(){
        return connection;
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <5; i++) {
            System.out.println(MyObject.connectionFactory.getConnection().hashCode());
        }
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

程序运行结果如图:

 

7.完善使用 enum 枚举类实现单例模式 

修改 MyObject.java 

public class MyObject {
    public enum MyEnumSingleton{
        connectionFactory;
        private Connection connection;

        private MyEnumSingleton() {
            try {
                System.out.println("创建了 MyObject 对象");
                String url = "jdbc:mysql://localhost:3306/spring_boot";
                String username = "root";
                String password = "123456";
                String driverName = "com.mysql.cj.jdbc.Driver";
                Class.forName(driverName);
                connection  = DriverManager.getConnection(url,username,password);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        private Connection getConnection(){
            return connection;
        }
    }
    public static Connection getConnection(){
        return MyEnumSingleton.connectionFactory.getConnection();
    }
    
}

更改 MyThread.java 类的代码:

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <5; i++) {
            System.out.println(MyObject.getConnection().hashCode());
        }
    }
}

运行结果如图:

  


总结

加油!!!

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

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

相关文章

vue 中 js 金额数字转中文

参考&#xff1a;js工具函数之数字转为中文数字和大写金额_js封装工具类函数金额大写-CSDN博客 我使用的框架vol.core。 客户需求要将录入框的金额数字转换成中文在旁边显示&#xff0c;换了几种函数&#xff0c;最终确定如下函数 function changeToChineseMoney(Num) {//判断…

Quartz定时任务基础

springBoot有一个定时执行某个方法的 注解&#xff1a; Scheduled 可以满足挺多的需求&#xff0c;但是到了一些场景&#xff0c;就显得比较麻烦&#xff0c;比如&#xff1a; 机器待机五分钟后执行切换待机状态。如果是按照使用Scheduled注解&#xff0c;就得持久化一个表&…

【Java SE】 带你走近Java的抽象类与接口

&#x1f339;&#x1f339;&#x1f339;【JavaSE】专栏&#x1f339;&#x1f339;&#x1f339; &#x1f339;&#x1f339;&#x1f339;个人主页&#x1f339;&#x1f339;&#x1f339; &#x1f339;&#x1f339;&#x1f339;上一篇文章&#x1f339;&#x1f339;&…

2018年3月26日 Go生态洞察:Go包版本管理提案分析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

java springboot测试类虚拟MVC环境 匹配请求头指定key与预期值是否相同

上文 java springboot测试类虚拟MVC环境 匹配返回值与预期内容是否相同 (JSON数据格式) 版 中 我们展示 json匹配内容的方式 那么 本文我们来看看Content-Type属性的匹配方式 首先 我们从返回体可以看出 Content-Type 在请求头信息 Headers 中 我们直接将测试类代码更改如下 …

C#,《小白学程序》第二十七课:大数四则运算之“运算符重载”的算法及源程序

1 文本格式 using System; using System.Text; using System.Collections; using System.Collections.Generic; /// <summary> /// 大数的四则&#xff08;加减乘除&#xff09;运算 /// 及其运算符重载&#xff08;取余数&#xff09; /// </summary> public cl…

在项目中集成marsUI

拷贝文件夹到目标项目 集成 安装相关依赖 npm i --save ant-design-vue4.x npm i less npm i nprogress npm i consola npm i echarts npm i vue-color-kit npm i icon-park/svg npm i vite-plugin-style-import 配置Vite文件 使用 效果

Leetcode—828.统计子串中的唯一字符【困难】

2023每日刷题&#xff08;四十一&#xff09; Leetcode—828.统计子串中的唯一字符 算法思想 枚举所有种类字母在s中出现的位置&#xff0c;分别统计只包含这个字母不包含该类字母中其他字母的子串个数 实现代码 int uniqueLetterString(char* s) {int len strlen(s);cha…

电子学会C/C++编程等级考试2022年06月(二级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:小白鼠再排队 N只小白鼠(1 < N < 100),每只鼠头上戴着一顶有颜色的帽子。现在称出每只白鼠的重量,要求按照白鼠重量从小到大的顺序输出它们头上帽子的颜色。帽子的颜色用 “red”,“blue”等字符串来表示。不同的小白…

十分钟让你搞懂JVM中的GC垃圾回收机制(分代回收)

文章目录 0. 为什么要有垃圾回收?1. 垃圾回收哪个内存区域?2. 如何找到垃圾(死亡对象的判断)2.1 引用计数法2.2 可达性分析法2.3 两种算法的差别 3. 如何清理垃圾(死亡对象的回收)3.1 标记-清楚法3.2 复制法3.3 标记-整理法 4. JVM使用的回收方法4.1 什么是分代回收4.2 哪些对…

【Linux】:信号的产生

信号 一.前台进程和后台进程1.前台进程2。后台进程3.总结 二.自定义信号动作接口三.信号的产生1.键盘组合键2.kill信号进程pid3.系统调用1.kill函数2.raise函数3.abort函数 四.异常五.软件条件六.通过终端按键产生信号 一.前台进程和后台进程 1.前台进程 一个简单的代码演示 …

跟着chatgpt学习|1.spark入门

首先先让chatgpt帮我规划学习路径&#xff0c;使用Markdown格式返回&#xff0c;并转成思维导图的形式 目录 目录 1. 了解spark 1.1 Spark的概念 1.2 Spark的架构 1.3 Spark的基本功能 2.spark中的数据抽象和操作方式 2.1.RDD&#xff08;弹性分布式数据集&#xff09; 2…

JAVA时间常用操作工具类

小刘整理了JAVA中对时间的常用操作&#xff0c;封装了几种方法&#xff0c;简单方便&#xff0c;开箱即用。时间转字符串格式&#xff0c;字符串转时间&#xff0c;以及过去和未来的日期。除此之外&#xff0c;还新增了时间戳之差计算时分秒天的具体方案。 public static void …

【力扣:1707 1803】0-1字典树

思路&#xff1a;树上每个节点存储拥有该节点的数组元素的最小值&#xff0c;left节点表示0&#xff0c;right节点表示1&#xff0c;构建完成后遍历树当子节点没有比mi小的元素时直接输出-1&#xff0c;否则向下构造。 struct tree{int m;tree*leftnullptr,*rightnullptr;tree…

智能优化算法应用:基于海鸥算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于海鸥算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于海鸥算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.海鸥算法4.实验参数设定5.算法结果6.参考文献7.MATLAB…

养生馆服务预约会员管理系统小程序效果如何

中医养生馆的全国数量逐渐增加&#xff0c;各种疾病困扰下&#xff0c;有些病往往通过养生馆即可治好&#xff0c;比如常见的针灸、按摩、药理滋补、切脉等&#xff0c;都有很高的市场需求度&#xff0c;而随着众多商家入局赛道及消费升级&#xff0c;传统中医养生馆经营痛点也…

深度学习第3天:CNN卷积神经网络

☁️主页 Nowl &#x1f525;专栏《机器学习实战》 《机器学习》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​ 文章目录 介绍 CNN的主要结构 卷积层 激励层 池化层 Kears搭建CNN 搭建代码 直观感受卷积的作用 结语 介绍 卷积神经网络&#xff08;Convol…

印刷基板开孔机上的直线导轨怎么安装?

直线导轨是属于高精度的传动元件&#xff0c;作为印刷基板开孔机重要的传动元件&#xff0c;倘若安装不当&#xff0c;严重则无法正常作业&#xff0c;轻则影响直线导轨的精度和寿命。那么&#xff0c;印刷基板开孔机的直线导轨是如何安装的呢&#xff1f; 在安装前&#xff0c…

C语言编译过程再解析

多年以前,分析过编译过程,并写了一篇博客,现在对编译过程有了更广阔的认识,记录在此 编译过程 中的 链接与 编译 编译过程分为1. 预处理2. 编译3. 汇编4. 链接其中有 2个过程比较特殊,1. 编译2. 链接对于C程序来说,链接分为提前链接(静态链接)对应下图第1行运行时链接(动态链…

Spring Boot 改版如何解决?使用阿里云创建项目、使用IDEA进行创建

接上次博客&#xff1a;JavaEE进阶&#xff08;2&#xff09;SpringBoot 快速上手&#xff08;环境准备、Maven&#xff1a;核心功能&#xff0c;Maven仓库、第⼀个SpringBoot程序&#xff1a;Spring介绍&#xff0c;Spring Boot介绍、创建项目&#xff09;-CSDN博客 目录 使…