【菜鸟学院】精通Spring框架——04单例模式

26 篇文章 2 订阅
本文详细探讨了单例模式在Java中的应用场景,包括饿汉式、懒汉式、双重检查锁定、静态内部类及枚举实现。通过实例分析了线程安全、反射与序列化对单例的影响,并介绍了Spring中的ApplicationContext作为单例模式的应用。同时,讨论了ThreadLocal在实现线程局部单例的作用。
摘要由CSDN通过智能技术生成

源码:https://github.com/2020GetGoodOffer/springStudy


单例模式的应用场景

单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
单例模式是创建型模式,单例模式在现实中的应用如公司的CEO、部门经理等。
J2EE标准中的ServletContext、ServletContextConfig等、Spring中的ApplicationContext、数据库的连接池等也都是单例模式。


饿汉式单例模式

饿汉式单例模式在类加载的时候就立即初始化并且创建单例对象,它绝对线程安全,在线程出现以前就实例化了,不可能存在访问安全问题。
优点:
没有加任何锁、执行效率比较高、用户体验比懒汉式单例模式更好
缺点:
类加载时就初始化,不管使用与否都占空间,浪费了内存

Spring中IoC容器ApplicationContext本身就是典型的饿汉式单例模式,接下来看一段代码:

public class HungrySingleton {
    //私有唯一实例
    private static final HungrySingleton hungrySingleton=new HungrySingleton();
    //私有构造方法
    private HungrySingleton(){}
    //公有获取实例方法
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

还有另一种写法,利用静态代码块的机制:

public class HungryStaticSingleton {
    //私有唯一实例
    private static final HungryStaticSingleton hungryStaticSingleton;
    static {
        hungryStaticSingleton=new HungryStaticSingleton();
    }
    //私有构造方法
    private HungryStaticSingleton(){}
    //公有获取实例方法
    public static HungryStaticSingleton getInstance(){
        return hungryStaticSingleton;
    }
}

这两种写法都非常简单,也很容易理解,饿汉式单例模式适用于单例对象较少的情况。


懒汉式单例模式

懒汉式的特点是:被外部类调用的时候内部类才会加载,下面来看懒汉式单例模式的简单实现:

public class LazySimpleSingleton {
    
    //私有构造方法
    private LazySimpleSingleton(){};
    //私有唯一实例
    private static LazySimpleSingleton singleton=null;
    //公有获取实例方法

    public static LazySimpleSingleton getSingleton() {
        if(singleton==null)
            singleton=new LazySimpleSingleton();
        return singleton;
    }
}

然后写一个线程类ExecutorThread:

public class ExecutorThread implements Runnable {
    @Override
    public void run() {
        LazySimpleSingleton simpleSingleton=LazySimpleSingleton.getSingleton();
        System.out.println(Thread.currentThread().getName()+" "+simpleSingleton);
    }

}

测试代码如下:

	public static void main(String[] args) {
        Thread t1 = new Thread(new ExecutorThread());
        Thread t2 = new Thread(new ExecutorThread());
        t1.start();
        t2.start();
    }

运行结果如下:
在这里插入图片描述
在这里插入图片描述
通过第二次运行的结果,我们可以发现两个对象并不是一个,所以懒汉式存在线程安全问题。
接下来我们来修改代码,使用synchronized关键字保证线程的同步:

public static synchronized LazySimpleSingleton getSingleton() {
        if(singleton==null)
            singleton=new LazySimpleSingleton();
        return singleton;
    }

此时当其中一个线程调用getInstance方法时,另一个线程如何再调用该方法其状态就会从RUNNING变为MONITOR,出现阻塞,直到第一个线程执行完。
此时线程安全的问题解决了,但是synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。
那么我们希望可以找到一种既能兼顾线程安全,又能提升程序性能的方式。
对此,我们可以使用双重锁机制

public static LazySimpleSingleton getSingleton() {
        if(singleton==null) {
            synchronized (LazySimpleSingleton.class) {
                if(singleton==null)
                    singleton = new LazySimpleSingleton();
            }
        }
        return singleton;
    }

此时当一个线程调用getInstance时,另一个线程也可以调用。只是在执行到synchronized时会阻塞,此时阻塞并不是基于整个LazySimpleSingleton的阻塞,而是再getInstance方法内部的阻塞,只要逻辑不太复杂,对于调用者而言是感知不到的。

但是使用synchronized总归要上锁,对程序的性能还是存在一定影响的。来看以下代码,从类初始化的角度考虑,采用静态内部类的方式:

public class LazyInnerClassSingleton {
    //使用时默认初始化内部类,没有使用则不加载
    private LazyInnerClassSingleton(){};
    
    public static LazyInnerClassSingleton getInstance(){
        //在返回结果前会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY=new LazyInnerClassSingleton();
    }
    
}

这种方式兼顾了饿汉式单例模式浪费内存和synchronized的性能问题,内部类一定是在方法调用之前初始化,避免了线程安全问题。


反射破坏单例

之前的单例模式的构造方法除了加上了private关键字,并没有做其他处理。
如果我们使用反射调用构造方法,再掉用getInstance方法,就应该会有两个不同的实例,以LazyInnerClassSingleton为例:

class Test{
    public static void main(String[] args) {
        try {
            Class<?> clazz = LazyInnerClassSingleton.class;
            Constructor c = clazz.getDeclaredConstructor();
            //强制访问
            c.setAccessible(true);
            Object o1 = c.newInstance();
            Object o2 = c.newInstance();
            System.out.println(o1==o2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述
显然,创建了两个不同的实例。
现在我们在其构造方法中做一些限制,一旦出现多次创建直接抛出异常:

public class LazyInnerClassSingleton {
    //使用时默认初始化内部类,没有使用则不加载
    private LazyInnerClassSingleton(){
        throw new RuntimeException("不允许创建多个实例!");
    };

    public static LazyInnerClassSingleton getInstance(){
        //在返回结果前会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY=new LazyInnerClassSingleton();
    }

}

此时如果进行测试会报错:
在这里插入图片描述
如果是通过公有静态方法获取实例则没有问题
在这里插入图片描述


序列化破坏单例

一个单例对象创建好后,有时需要将对象序列化然后写入磁盘,下次使用时再从磁盘读取对象并进行反序列化,将其转为内存对象。
反序列化后的对象会重新分配内存,即重新创建。
如果序列化的目标对象为单例模式,就违背了单例模式的初衷,相当于破坏了单例模式。
来看以下代码:

public class SerSingleton implements Serializable {
    
    public final static SerSingleton SINGLETON=new SerSingleton();
    
    private SerSingleton(){}
    
    public static SerSingleton getInstance(){
        return SINGLETON;
    }
}

测试代码:

public class Test {

    public static void main(String[] args) throws Exception{
        SerSingleton s1=null;
        SerSingleton s2=SerSingleton.getInstance();
        //对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
        //写出对象
        oos.writeObject(s2);
        //刷新流
        oos.flush();
        //释放资源
        oos.close();
        
        //对象输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SerSingleton.obj"));
        //读入对象
        s1= (SerSingleton) ois.readObject();
        //释放资源
        ois.close();

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

运行结果:
在这里插入图片描述
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例。
那么如何保证序列化情况下也能实现单例模式呢,可以使用readResolve方法:

public class SerSingleton implements Serializable {

    public final static SerSingleton SINGLETON=new SerSingleton();

    private SerSingleton(){}

    public static SerSingleton getInstance(){
        return SINGLETON;
    }
    
    private Object readResolve(){
        return SINGLETON;
    }
}

此时运行结果:
在这里插入图片描述
这是什么原因呢?
我们先来看看ObjectInputStream中的readObject方法:
在这里插入图片描述
我们发现在readObject方法中又调用了重写的readObject0方法,该方法中有以下代码:
在这里插入图片描述
该方法中调用了isInstantiable()方法
在这里插入图片描述
isInstantiable()的代码如下:

在这里插入图片描述
这个代码非常简单,就是判断构造方法是否为空,不为空就返回true,这意味着只要有无参构造方法就会实例化。
这时还没有找到加上readResolve方法就能避免单例模式被破坏的真正原有,再回到ObjectInputStream的readOridinaryObject方法继续看:
在这里插入图片描述
再判断无参构造方法是否存在后,又调用了hasReadResolveMethod方法:
在这里插入图片描述
该方法就是判断readResolveMethod是否为空,不为空就返回true。那么readResolveMehod方法是在哪赋值的呢,在ObjectStramClass的私有构造方法ObjectStramClass中给readResolveMethod进行了赋值:
在这里插入图片描述
上面代码的逻辑就是通过反射找到一个无参的readResolve方法并且保存下来。
现在回到ObjectInputStram的readOrdinaryObject方法继续往下看,如果readResolve方法存在则调用invokeReadResolve方法:
在这里插入图片描述
在invokeReadResolve方法中反射调用了readResolveMethod方法
在这里插入图片描述
通过JDK源码我们可以看出,虽然增加readResolve方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只是新创建的对象没有被返回而已。
如果创建对象的动作发生频率高,就意味着内存分配开销也会随之增大,那么还有什么从根本解决问题的方法吗?接着往下看


注册单例模式

注册单例模式又称登记式单例模式,就是将每一个单例都登记到某一个地方,使用唯一的标识获取实例。
注册式单例模式有两种,一种为枚举式单例模式,一种为容器式单例模式。

枚举式单例模式

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

测试代码:

public class Test {

    public static void main(String[] args) {
        try {
            EnumSingleton singleton1;
            EnumSingleton singleton2=EnumSingleton.getInstance();
            singleton2.setData(new Object());
            //对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
            //写出对象
            oos.writeObject(singleton2);
            //刷新流
            oos.flush();
            //释放资源
            oos.close();

            //对象输入流
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
            //读入对象
            singleton1= (EnumSingleton) ois.readObject();
            //释放资源
            ois.close();

            System.out.println(singleton1.getData());
            System.out.println(singleton2.getData());
            System.out.println(singleton1.getData()==singleton2.getData());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述
没有做任何处理,我们发现运行结果和预期一样,那么原因是什么呢?
通过反编译EnumSingleton.class我们可以发现如下代码:

static{
	INSTANCE=new EnumSingleton("INSTANCE",0);
	$VALUES=new EnumSingleton[]{
				INSTANCE}
			);
}

原来枚举式单例模式在静态代码块中就给INSTANCE进行赋值,是饿汉式单例模式的实现。至此我们还可以试想序列化能否破坏枚举式单例模式呢,我们再看一次ObjectInputStream中的readObject0方法:
在这里插入图片描述
我们看到其中调用了readEnum方法:
在这里插入图片描述
我们可以看到枚举类型其实通过类名和类对象类找到了唯一一个枚举对象,因此枚举对象不可能被类加载器加载很多次。
那么反射能否破坏枚举式单例模式呢?

public static void main(String[] args) throws Exception {
        Class<?> clazz=EnumSingleton.class;
        clazz.getDeclaredConstructor().newInstance();
    }

运行以上代码:
在这里插入图片描述
结果中报的是没有该方法的异常,也就是说没有找到无参构造方法。我们打开Enum的源码,查看它的构造方法,只有一个proteced类型的构造方法。
在这里插入图片描述
我们再来做以下测试

	public static void main(String[] args) throws Exception {
        Class<?> clazz=EnumSingleton.class;
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSingleton singleton= (EnumSingleton) constructor.newInstance("Kobe",24);
    }

运行结果:
在这里插入图片描述
可以看到是不可以通过反射来创建枚举类型的,我们来看看Constructor的newInstance方法:
在这里插入图片描述
可以看到如果修饰符是枚举类型直接抛出异常。
枚举式单例模式也是Effective Java中推荐的一种单例模式写法。


容器式单例模式

以下是注册式单例模式的代码:

public class ContainerSingleton {
    private ContainerSingleton(){}
    
    private static final Map<String,Object> ioc=new ConcurrentHashMap<>();
    
    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object object=null;
                try{
                    object=Class.forName(className).getConstructor().newInstance();
                    ioc.put(className,object);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return object;
            }else 
                return ioc.get(className);
        }
    }
}

注册式单例模式适用于单例非常多的情况,便于管理。
但是它是非线程安全的。
Spring中的容器式单例模式:
在这里插入图片描述


线程单例实现ThreadLocal

ThreadLocal不能保证其创建的对象是全局唯一的,但是可以保证在单个线程中是唯一的,天生是线程安全的。

public class ThreadLocalSingleton {
    
    //唯一实例对象
    private static final ThreadLocal<ThreadLocalSingleton> INSTANCE= ThreadLocal.withInitial(ThreadLocalSingleton::new);
    
    //私有构造方法
    private ThreadLocalSingleton(){}
    
    //公有获取方法
    public static ThreadLocalSingleton getInstance(){
        return INSTANCE.get();
    }
}

测试代码:

public static void main(String[] args) {
       System.out.println(ThreadLocalSingleton.getInstance());
       System.out.println(ThreadLocalSingleton.getInstance());
       System.out.println(ThreadLocalSingleton.getInstance());
       System.out.println(ThreadLocalSingleton.getInstance());
       System.out.println(ThreadLocalSingleton.getInstance());
       new Thread(new TLThread()).start();
       new Thread(new TLThread()).start();

}


class TLThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
    }
}

运行结果:
在这里插入图片描述

我们发现在相同的线程中无论调用多少次,获取到的实例都是同一个。
单例模式为了达到线程安全会给方法上锁,以时间换空间。
ThreadLocal将所有对象都放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。


总结

单例模式可以保证内存只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值