Java多线程--解决单例模式中的懒汉式的线程安全问题

news2024/10/7 10:16:01

文章目录

  • 一、单例设计模式的线程安全问题
    • (1)饿汉式没有线程安全问题
    • (2)懒汉式线程安全问题
      • 1、案例
      • 2、方式1-同步方法
      • 3、方式2-同步代码块
      • 4、优化
  • 二、代码
    • (1)实现线程安全的懒汉式
    • (2)使用内部类

一、单例设计模式的线程安全问题

单例设计模式博客链接:https://blog.csdn.net/m0_55746113/article/details/134492961

  • 饿汉式:不存在线程安全问题。
  • 懒汉式:存在线程安全问题,(需要使用同步机制来处理)

(1)饿汉式没有线程安全问题

饿汉式:在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的。

🍰格式

class Singleton {
    // 1.私有化构造器
    private Singleton() {
    }

    // 2.内部提供一个当前类的实例
    // 4.此实例也必须静态化
    private static Singleton single = new Singleton();

    // 3.提供公共的静态的方法,返回当前类的对象
    public static Singleton getInstance() {
        return single;
    }
}

【举例】

形式一:

package com.atguigu.single.hungry;

public class HungrySingle {
    private static HungrySingle INSTANCE = new HungrySingle(); //对象是否声明为final 都可以
    
    private HungrySingle(){}
    
    public static HungrySingle getInstance(){
        return INSTANCE;
    }
}

形式二:

/*
public class HungryOne{
    public static final HungryOne INSTANCE = new HungryOne();
    private HungryOne(){}
}*/

public enum HungryOne{
    INSTANCE
}

测试类:

package com.atguigu.single.hungry;

public class HungrySingleTest {

    static HungrySingle hs1 = null;
    static HungrySingle hs2 = null;

    //演示存在的线程安全问题
    public static void main(String[] args) {

        Thread t1 = new Thread() {
            @Override
            public void run() {
                hs1 = HungrySingle.getInstance();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                hs2 = HungrySingle.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(hs1);
        System.out.println(hs2);
        System.out.println(hs1 == hs2);//true
    }

}

(2)懒汉式线程安全问题

懒汉式:延迟创建对象,第一次调用getInstance方法再创建对象。

🍰格式

class Singleton {
    // 1.私有化构造器
    private Singleton() {
    }
    // 2.内部提供一个当前类的实例
    // 4.此实例也必须静态化
    private static Singleton single;
    // 3.提供公共的静态的方法,返回当前类的对象
    public static Singleton getInstance() {
        if(single == null) {
            single = new Singleton();
        }
        return single;
    }
}

1、案例

【案例】

举个例子:

public class BankTest {

}

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }

    //先不造好,通过一个方法返回
    private static Bank instance = null;

    public static Bank getInstance() {
        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }
}

线程安全问题:若有两个线程去调用getInstance方法,他们主要的目的是为了获取instance实例,这个实例就相当于是“共享数据”。

image.png

若第一个线程判断if,发现是null,就进去了。假设此时被sleep阻塞了,然后Bank实例并没有被创建成功。

此时第二个线程也进入了if,判断一下发现是null,然后将Bank实例创建好了。

现在第一个线程阻塞结束,就会再创建一个Bank实例。

所以,instance = new Bank();语句被先后执行了两次,这显然就不是我们想看到的。


🗳️将刚才的问题用代码描述出来。

public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {

    }
}

将b1与b2设置为静态的,就可以直接在main方法中去调用他们了。

在static方法内部只能访问类的static修饰的属性和方法,不能访问类的非static结构。

现在提供两个线程,让他们去调用getInstance方法。

就整一个匿名子类的对象吧:

Thread t1=new Thread(){
    //重写
};

run方法里面,通过Bank调用getInstance方法,如下:

public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance();	//将方法返回的对象赋给b1
            }
        };
        t1.start();
    }
}

再创建一个线程t2

public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance(); //将方法返回的对象赋给b1
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                b2=Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

    }
}

线程先后调用getInstance()方法,返回instance方法,地址值依次给了b1和b2。

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static Bank instance = null;

    public static Bank getInstance() {
        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }
}

来看看b1与b2的地址值是不是一样的,如下:

public class BankTest {
    	//...
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

按正常情况来说,两者的地址值应该是相等的,要不然也不能叫单例设计模式了。

但是实际情况也有可能不等,为了让问题突出一点,在这个地方加一个sleep

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static Bank instance = null;

    public static Bank getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
        return instance;
    }
}

这样就将问题放大了,看看会不会出现安全问题,如下:

image.png

🎲为什么都是null呢?

两个分线程出去了,主线程还在执行,之所以是null,那是因为主线程先执行了,两个分线程还没有调用getInstance方法,所以就null了。

image.png

有一种方式是在调用start方法的时候,让主线程睡一会,给充分的时间让分线程去执行,但是睡一会也不一定靠谱啊。

最靠谱的就是执行join操作,保证两个分线程执行完之后,主线程才继续向后执行。如下:

t1.start();
t2.start();

t1.join();
t2.join();

分别处理一下异常:

t1.start();
t2.start();

try {
    t1.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

try {
    t2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

现在这两个分线程执行完之后,主线程才会继续往下去执行。

void join() :等待该线程终止。

在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。

🌱代码

public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance(); //将方法返回的对象赋给b1
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                b2=Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static Bank instance = null;

    public static Bank getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
        return instance;
    }
}

🍺输出结果

image.png

可以看到,两个线程的地址值一样。

是不是巧合呢?

将睡眠时间改为1000ms,如下:

image.png

地址不一样了,这就是线程的不安全性。单例,怎么会有两个呢?

2、方式1-同步方法

现在需要解决线程的不安全问题,主要针对的就是这个方法:

image.png

现在的共享数据是instance,发现操作instance的代码被完全放在getInstance方法里面了。

可以考虑将这个方法声明为同步方法,如下:

image.png

同步方法的同步监视器改不了,是默认的。

此时getInstance方法是默认方法,它的同步监视器是当前类本身。即Bank.class

后面讲反射的时候会提到,Bank.class其实也是个对象,同步监视器一定是由对象来充当的

类加载它,在缓存中只会加载一次,所以是唯一的,所以此时线程安全

🌱代码

package yuyi04.singleton;

/**
 * ClassName: BankTest
 * Package: yuyi04.singleton
 * Description:
 * 实现线程安全的懒汉式
 *
 * @Author 雨翼轻尘
 * @Create 2024/1/31 0031 10:39
 */
public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance(); //将方法返回的对象赋给b1
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                b2=Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static Bank instance = null;

    public static synchronized Bank getInstance() { //同步监视器是Bank.class
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
        return instance;
    }
}

🍺输出结果

image.png

此时不管睡眠多久,得到的地址值一定是一样的。

可以看到,之前睡100s的时候,有执行正确的时候。

也就是虽然执行结果正确,但是存在出现错误的可能性。只是出现概率比较低,不一定能发现问题。

这就需要经验,你要知道这里边会出现相关问题,要提前预防这样的问题。

3、方式2-同步代码块

刚才方式1是把这个方法声明为同步方法了,如下:

image.png

其实我们还可以使用同步代码块

就是将他们包裹起来,如下:

image.png

快捷键Ctrl+Alt+T,选择第9个:

image.png

那么小括号里面些什么呢?要保证唯一性。

不可以写this,因为此时是静态方法。

所以还是这样来写Bank.class,如下:

//实现线程安全的方式2
public static  Bank getInstance() { //同步监视器是Bank.class
    synchronized (Bank.class) {
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
        return instance;
    }
}

这里和方法1没有本质区别,就是将代码主动用synchronized包裹了一下而已。

有一个小细节,这里将return instance;放在外面能稍微好一点。

//实现线程安全的方式2
public static  Bank getInstance() { //同步监视器是Bank.class
    synchronized (Bank.class) {
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }

    }
    return instance;
}

因为返回instance并没有修改instance的值,所以线程先后执行这条语句不影响结果。

🌱代码

public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance(); //将方法返回的对象赋给b1
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                b2=Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static Bank instance = null;

    //实现线程安全的方式2
    public static  Bank getInstance() { //同步监视器是Bank.class
        synchronized (Bank.class) {
            if (instance == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Bank();
            }

        }
        return instance;
    }
}

🍺输出结果

image.png

4、优化

观察一下下面的代码:

public static  Bank getInstance() { //同步监视器是Bank.class
    synchronized (Bank.class) {
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }

    }
    return instance;
}

多线程的“同步机制”:一个线程握着同步监视器,进入了同步代码块里面操作,后面的线程必须在第一个线程执行结束释放同步监视器之后才能进入同步代码块

现在来想个问题,第一个线程进入同步代码块之后,创建了instance,然后执行结束释放了同步监视器。

对于后面的线程来说,他们也不需要再去new Bank()了,因为此时instance不是null,后续线程进入同步代码块里面什么也不需要执行就出来了。

所以就希望后续线程调用同步代码块的时候,直接拿着已经创建好的instance走即可,不必要一直等着进去然后啥也没干就出来了。

现在就在方法2的基础之上稍微优化一下。

//实现线程安全的方式3
public static  Bank getInstance() { //同步监视器是Bank.class
    if (instance == null) {
        synchronized (Bank.class) {
            if (instance == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Bank();
            }

        }
    }
    return instance;
}

🎲现在在外面加了if,那么里面还需要再加吗?

需要的。

🍰分析

比如此时线程1执行getInstance方法,碰到了最外层的if判断,此时instance是null,然后它就进入这个if,接下来碰到了同步代码块,拿到锁并进入同步代码块。

接下来线程1执行sleep操作,也就是instance还没有被创建,也就意味着此时instance判断还是null。

若此时线程2也执行了getInstance方法,判断最外层的if,此时线程1还在sleep,没有创建instance,所以线程2判断instance是null,进入最外层的if,接下来碰到了同步代码块,但是锁被线程1拿走了,就只能在同步代码块外面等着。

直到线程1sleep结束,然后创建了instance,出了同步代码块,释放锁,线程2才能拿到锁进入同步代码块。

此时碰到了内层if,判断得知instance不是null了,因为线程1已经创建好了instance。

所以线程2就直接出了同步代码块。

若此时线程3也执行getInstance方法,判断最外层的if,因为线程1已经创建了instance,所以它就不用继续往下执行了,直接略过if,返回instance即可。

后面的线程就都不需要进入if了,直接返回instance即可。

这样就保证了instance的唯一。

如下:

image.png

后续的线程只要在instance被实例化以后,就不会再进入同步代码块了。

优化之后的代码效率会更高一些。


若是将内层的if删除

假设两个线程都进入if()后, 线程1先进入同步监视器,创建了一个新Bank ,线程1结束执行后,由于在同步监视器之前就已经判断好instance为nulll,那么线程2也会进入创建一个新对象,所以不安全。如下:

public static  Bank getInstance() { //同步监视器是Bank.class
    if (instance == null) {
        synchronized (Bank.class) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
    }
    return instance;
}

优化之后,相当于在实例创建完后,代码的执行就变成并行的而不是串行的了,线程之间不用再争夺锁,相当于繁琐了实例创建前的步骤而简化了实例创建后的步骤。

🌱代码

public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance(); //将方法返回的对象赋给b1
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                b2=Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static Bank instance = null;

    //实现线程安全的方式3:相较于方式1和方式2来说,效率更高
    public static  Bank getInstance() { //同步监视器是Bank.class
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Bank();
                }

            }
        }
        return instance;
    }
}

🍺输出结果

image.png


☕注意:上述方式3中,有指令重排问题。

比如线程1进入if,判断instance是null,然后拿到同步锁,进入同步代码块,碰到了sleep睡了。

此时instance还没有创建,所以线程2可以进入最外层的if,但是进不去同步代码块。

然后线程1sleep结束后,执行instance=new Bank;的操作。此时对象已经创建了(instance已经不是null了),但是有可能还没有执行以构造器为代表的init方法,也就是对象已经有了,但是还没有初始化完成,没有真正执行完所有的步骤。(初始化分为好多步骤,有一个环节就是已经创建好了对象)

因为有指令重排,所以在没有初始化完成的时候,线程1它有出去的可能,虽然没有完全执行完init方法,但是对象已经有了。

若此时线程2拿到了锁进入同步代码块,发现instance不是null,就会直接略过内层if,直接return了现在的instance,但此时的instance还没有初始化完成,就会有风险。

mem = allocate(); 为单例对象分配内存空间
instance = mem;   instance引用现在非空,但还未初始化
ctorSingleton(instance); 为单例对象通过instance调用构造器

从JDK2开始,分配空间、初始化、调用构造器(对象创建的过程)会在线程的工作存储区一次性完成,然后复制到主存储区。
但是需要volatile关键字修饰,避免指令重排

🚗解决方案

instance前面加上关键字volatile即可避免重排问题。

如下:

image.png

代码:

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static volatile Bank instance = null;

    //实现线程安全的方式3:相较于方式1和方式2来说,效率更高;为了避免指令重排,需要将instance声明为volatile
    public static  Bank getInstance() { //同步监视器是Bank.class
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Bank();
                }

            }
        }
        return instance;
    }
}

创建对象过程:1.分配内存空间 2.初始化对象 3.引用指向刚分配的内存空间,JDK2.0起,为了优化,调整顺序1->3->2,即指令重排,这带来隐患,引用不为null但还没初始化对象。

二、代码

(1)实现线程安全的懒汉式

package yuyi04.singleton;

/**
 * ClassName: BankTest
 * Package: yuyi04.singleton
 * Description:
 * 实现线程安全的懒汉式
 *
 * @Author 雨翼轻尘
 * @Create 2024/1/31 0031 10:39
 */
public class BankTest {
    static Bank b1=null;   //为了在main方法中调用的时候,不使用对象来调用(方便一点),这里就设置成静态的
    static Bank b2=null;

    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                b1=Bank.getInstance(); //将方法返回的对象赋给b1
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                b2=Bank.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

class Bank {
    //私有化构造器
    private Bank() {     //这里不提供实例变量了

    }
    private static volatile Bank instance = null;

    //实现线程安全的方式1
    /*public static synchronized Bank getInstance() { //同步监视器是Bank.class
        if (instance == null) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
        return instance;
    }*/

    //实现线程安全的方式2
    /*public static  Bank getInstance() { //同步监视器是Bank.class
        synchronized (Bank.class) {
            if (instance == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Bank();
            }

        }
        return instance;
    }*/

    //实现线程安全的方式3:相较于方式1和方式2来说,效率更高;为了避免指令重排,需要将instance声明为volatile
    public static  Bank getInstance() { //同步监视器是Bank.class
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Bank();
                }

            }
        }
        return instance;
    }
}

(2)使用内部类

package com.atguigu.single.lazy;

public class LazySingle {
    private LazySingle(){}
    
    public static LazySingle getInstance(){
        return Inner.INSTANCE;
    }
    
    private static class Inner{
        static final LazySingle INSTANCE = new LazySingle();
    }
    
}

内部类只有在外部类被调用才加载,产生INSTANCE实例;又不用加锁。

此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。

此时的内部类,使用enum进行定义,也是可以的。

测试类:

package com.atguigu.single.lazy;

import org.junit.Test;

public class TestLazy {
    @Test
    public void test01(){
        LazyOne s1 = LazyOne.getInstance();
        LazyOne s2 = LazyOne.getInstance();

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    }

    //把s1和s2声明在外面,是想要在线程的匿名内部类中为s1和s2赋值
    LazyOne s1;
    LazyOne s2;
    @Test
    public void test02(){
        Thread t1 = new Thread(){
            public void run(){
                s1 = LazyOne.getInstance();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                s2 = LazyOne.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);
    }


    LazySingle obj1;
    LazySingle obj2;
    @Test
    public void test03(){
        Thread t1 = new Thread(){
            public void run(){
                obj1 = LazySingle.getInstance();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                obj2 = LazySingle.getInstance();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(obj1);
        System.out.println(obj2);
        System.out.println(obj1 == obj2);
    }
}

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

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

相关文章

【力扣白嫖日记】SQL

前言 练习SQL语句,所有题目来自于力扣(https://leetcode.cn/problemset/database/)的免费数据库练习题。 今日题目: 1387.使用唯一标识码替代员工ID 表:Employees 列名类型idintnamevarchar 在 SQL 中&#xff0c…

k8s安装dashboard报错CrashLoopBackOff

报错信息 使用kubectl get pods -A查看集群,出现错误: kubernetes-dashboard kubernetes-dashboard-xxxxxxxxxx6-2qrst 0/1 CrashLoopBackOff 6 15m查看日志后,发现原因: panic: Get "https://10…

Pandas 数据结构 – Pandas CSV 文件

Pandas CSV 文件 CSV(Comma-Separated Values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。 CSV 是一种通用的、…

【IC设计】Windows下基于IDEA的Chisel环境安装教程(图文并茂)

Chisel环境安装教程 第一步 安装jdk,配置环境变量第二步 安装sbt,不用配置环境变量第三步 安装idea社区版第四步 离线安装scala的idea插件第五步 配置sbt换源1.切换目录2.创建repositories文件3.配置sbtconfig.txt文件 第六步 使用chisel-tutorial工程运…

Android矩阵setRectToRect裁剪Bitmap原图Matrix放大,mapRect标记中心区域,Kotlin

Android矩阵setRectToRect裁剪Bitmap原图Matrix放大,mapRect标记中心区域,Kotlin import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix impo…

数学知识第八期 组合数全集

前言:组合数在高中大家应该都学过,也是一个重要的数学知识,希望大家能够完全掌握 一、组合数基本知识 定义: 组合是数学的重要概念之一。从 n 个不同元素中每次取出 m 个不同元素 ,不管其顺序合成一组,称…

YIA主题如何关闭新版本升级提示?WordPress主题怎么取消升级提醒?

前两天YIA主题发布了升级到2.8版本,新增了一些功能,优化调整修复了一些功能,但是这些功能调整幅度不大,加上boke112百科使用的YIA主题已经进行了很多方面的个性化修改,所以就懒得升级了,但是每次进入WordPr…

C++——数据类型

C——数据类型 1.基本变量类型 C 基本数据类型整理成表格。以下是一个表格&#xff0c;展示了不同的基本数据类型及其一般用途和大小范围&#xff1a;和C语言类似。 2.宽字符的用法 #include <iostream> #include <locale> #include <wchar.h> int main…

Vue(二十):ElementUI 扩展实现表格组件的拖拽行

效果 源码 注意&#xff1a; 表格组件必须添加 row-key 属性&#xff0c;用来优化表格的渲染 <template><el-row :gutter"10"><el-col :span"12"><el-card class"card"><el-scrollbar><span>注意: 表格组件…

FreeRTOS_Stm32F407系列单片机标准库移植

这里写目录标题 1、下载FreeRTOS源码1.1github仓库下载1.2官网下载1.3百度网盘下载 2、FreeRTOS移植2.1首先需要有一个可运行的标准库工程2.2在工程内创建一个FreeRTOS文件夹&#xff0c;然后在FreeRTOS文件夹中再新建port、include、src三个文件夹。2.3 port文件夹移植2.4 inc…

Android使用ScrollView导致鼠标点击事件无效

平台 测试平台: RK3288 Android8.1RK3588 Android 12 问题 首先, 这个问题的前提是, 使用的输入设备是**鼠标**, 普通的触摸屏并不会出现这个问题. 大致的流程是APP的UI布局中采用ScrollView作为根容器, 之后添加各类子控件, 在一起准备就绪后, 使用鼠标进行功能测试, 出现…

AD24-固定孔放置

1、固定孔放置的一般距离&#xff0c;分为金属和非金属 2、固定孔通过焊盘完成&#xff0c;放置焊盘&#xff0c;并将层修改为Multi Layer 焊盘与固定孔的等大小的 3、金属与非金属的区别 ①非金属 ②金属 4、设置固定孔放置的距离 5、通过复制粘贴即可完成其他孔的放置 6、导…

时间序列预测模型实战案例(二)(Holt-Winter)(Python)结合K-折交叉验证进行时间序列预测实现企业级预测精度(包括运行代码以及代码讲解)

目录 引言 数据格式 运行代码 Holt-Winters模型主体 程序入口 参数讲解 开始训练 预测结果 引言 话不多说上来先上预测精度分析图,其中MAE的误差大概在0.11,以下数据均是预测未知数据&#xff0c;而不是训练数据的预测图。 开始之前我们先来简单了解一下Holt-Winters…

数据结构+算法(第02篇):玩扫雷就是优化算法

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 学习必须往深处挖&…

网安渗透攻击作业(2)

sql注入第一关 一、部署 1、环境安装 &#xff08;1&#xff09;下载phpstudy&#xff0c;下载链接&#xff1a;小皮面板(phpstudy) - 让天下没有难配的服务器环境&#xff01; &#xff0c;安装过后打开软件进入如下界面&#xff0c;接着我们开启nginx和mysql 注意&#x…

Linux--Shell基础

学习笔记&#xff0c;记录以下课程中关于Linux的Shell基础知识。 黑马程序员linux入门到精通&#xff08;下部分&#xff09;_哔哩哔哩_bilibili 目录 1.编写规范 2.变量 2.1 变量的含义 2.2 变量的定义和使用 2.3 只读变量&#xff08;了解&#xff09; 2.4 接收用户输入…

Linux 命令 —— top

Linux 命令 —— top 相对于 ps 是选取一个时间点的进程状态&#xff0c;top 则可以持续检测进程运行的状态。使用方式如下&#xff1a; 用法&#xff1a; top [-d secs] | [-p pid] 选项与参数&#xff1a; -d secs&#xff1a;整个进程界面更新 secs 秒。默认是 5 5 5 秒。…

遇到ubuntu设置交叉编译环境的问题

今天交叉编译器一直没安装成功&#xff0c;环境变量也配置了还是不对&#xff0c;最后发现Ubuntu是64位的要装 然后就好了 另外在进行嵌入式Linux开发的时候&#xff0c;要把主机、虚拟机、以及开发板设置在同一网段下&#xff0c;虚拟机一般设成临时的就可以&#xff0c;但是…

力扣之2648.生成 斐波那契数列(yield)

/*** return {Generator<number>}*/ var fibGenerator function*() {let a 0,b 1;yield 0; // 返回 0&#xff0c;并暂停执行yield 1; // 返回 1&#xff0c;并暂停执行while(true) {yield a b; // 返回 a b&#xff0c;并暂停执行[a, b] [b, a b]; // 更新 a 和 …

使用Pycharm在本地调用chatgpt的接口

目录 1.安装环境 2.建立多轮对话的完整代码&#xff08;根据自己使用的不同代理需要修改端口&#xff08;port&#xff09;&#xff09; 3.修改代码在自己的Pycharm上访问chagpt的api并实现多轮对话&#xff0c;如果不修改是无法成功运行的。需要确定秘钥和端口以保证正常访…