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

文章目录

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

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

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

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

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

🍰格式

java 复制代码
class Singleton {
    // 1.私有化构造器
    private Singleton() {
    }

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

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

【举例】

形式一:

java 复制代码
package com.atguigu.single.hungry;

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

形式二:

java 复制代码
/*
public class HungryOne{
    public static final HungryOne INSTANCE = new HungryOne();
    private HungryOne(){}
}*/

public enum HungryOne{
    INSTANCE
}

测试类:

java 复制代码
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方法再创建对象。

🍰格式

java 复制代码
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、案例

【案例】

举个例子:

java 复制代码
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实例,这个实例就相当于是"共享数据"。

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

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

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

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


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

java 复制代码
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方法。

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

java 复制代码
Thread t1=new Thread(){
    //重写
};

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

java 复制代码
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

java 复制代码
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。

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

    }
    private static Bank instance = null;

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

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

java 复制代码
public class BankTest {
    	//...
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1==b2);

    }
}

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

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

java 复制代码
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;
    }
}

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

🎲为什么都是null呢?

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

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

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

java 复制代码
t1.start();
t2.start();

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

分别处理一下异常:

java 复制代码
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才结束阻塞状态,继续执行。

🌱代码

java 复制代码
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;
    }
}

🍺输出结果

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

是不是巧合呢?

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

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

2、方式1-同步方法

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

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

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

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

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

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

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

🌱代码

java 复制代码
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;
    }
}

🍺输出结果

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

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

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

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

3、方式2-同步代码块

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

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

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

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

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

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

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

java 复制代码
//实现线程安全的方式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;放在外面能稍微好一点。

java 复制代码
//实现线程安全的方式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的值,所以线程先后执行这条语句不影响结果。

🌱代码

java 复制代码
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;
    }
}

🍺输出结果

4、优化

观察一下下面的代码:

java 复制代码
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的基础之上稍微优化一下。

java 复制代码
//实现线程安全的方式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的唯一。

如下:

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

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


若是将内层的if删除

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

java 复制代码
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;
}

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

🌱代码

java 复制代码
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;
    }
}

🍺输出结果


☕注意:上述方式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还没有初始化完成,就会有风险。

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

从JDK2开始,分配空间、初始化、调用构造器 (对象创建的过程)会在线程的工作存储区一次性完成,然后复制到主存储区。

但是需要volatile关键字修饰,避免指令重排

🚗解决方案

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

如下:

代码:

java 复制代码
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)实现线程安全的懒汉式

java 复制代码
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)使用内部类

java 复制代码
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进行定义,也是可以的。

测试类:

java 复制代码
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);
    }
}
相关推荐
巨大八爪鱼2 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring
数据小小爬虫4 小时前
如何用Java爬虫“偷窥”淘宝商品类目API的返回值
java·爬虫·php