带你彻底搞懂设计模式之单例模式

1.单例模式的定义以及应用场景

        单例模式(SingLeton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局的访问点。单例模式属于创建型模式。单例模式在现实生活中也应用的非常广泛,例如公司的CEO。在J2EE标准中的ServletContext ,Spring框架中的ApplicationContext、数据库连接池等都用到了单例模式

2.饿汉式单例

        先来看看饿汉式单例的类图

                

 饿汉式单例是指在类初始化的时候就加载,并且创造单例对象。它绝对线程安全,在线程还没有出现之前就已经初始化了,不可能存在访问安全问题。    

我们来看看饿汉式单例标准写法:

/**
 * @author sj
 * @date 2022/12/8 14:40
 */
public class HungrySingleton {
    // static修饰在类加载时就会进行初始化
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    // 私有化构造方法
    private HungrySingleton() {
    }

    // 提供一个全局访问点
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }

}

还有一种写法,饿汉式静态单例,装个逼用

/**
 * @author sj
 * @date 2022/12/8 14:47
 */
public class HungryStaticSingleton {
    private HungryStaticSingleton() {
    }

    private static final HungryStaticSingleton INSTANCE;
    
    static {
        INSTANCE = new HungryStaticSingleton();
    }

    public static HungryStaticSingleton getInstance() {
        return INSTANCE;
    }
}

        这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说不管对象用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。那有没有更优的写法呢?下面我们来继续分析

3 懒汉式单例

        为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,下面看懒汉式单例模式的简单实现LazySimpleSingleton :

/**
 * @author sj
 * @date 2022/12/8 14:51
 */
public class LazySimpleSingleton {
    private LazySimpleSingleton() {
    }

    private static LazySimpleSingleton LAZY_INSTANCE = null;

    public static LazySimpleSingleton getInstance() {
        if (LAZY_INSTANCE == null) {
            LAZY_INSTANCE = new LazySimpleSingleton();
        }
        return LAZY_INSTANCE;
    }
}

但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我先来模拟一下编写线程类 ExecutorThread:

import Lazy.LazySimpleSingleton;

/**
 * @author sj
 * @date 2022/12/8 14:59
 */
public class ExecutorThread implements Runnable{
    @Override
    public void run() {
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + instance);
    }
}

        测试如下:

/**
 * @author sj
 * @date 2022/12/8 15:08
 */
public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new ExecutorThread());
        Thread thread2 = new Thread(new ExecutorThread());
        thread.start();
        thread2.start();
        System.out.println("结束");
    }
}

我们来看看结果:

 果然,上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患。我们通过调试运行再具体看一下。这里教大家一种新技能,用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化。先给 ExecutorThread类打上断点,如下图所示:

 

使用鼠标右键单击断点,切换为 Thread 模式,如下图所示:

 然后给 LazySimpleSingleton 类打上断点,同样标记为 Thread 模式,如下图所示:

 切回客户端测试代码,同样也打上断点,同时改为 Thread 模式,如下图所示:

开始“Debug”之后,会看到 Debug 控制台可以自由切换 Thread 的运行状态,如下图所示: 

通过不断切换线程,并观测其内存状态,我们发现在线程环境下 LazySimpleSingleton 被实例化了两次。有时我们得到的运行结果可能是相同的两个对象,实际上是被后面执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码给 getlnstance0加上 synchronized 关键字,使这个方法变成线程同步方法 


/**
 * @author sj
 * @date 2022/12/8 14:51
 */
public class LazySimpleSingleton {
    private LazySimpleSingleton() {
    }

    private static LazySimpleSingleton LAZY_INSTANCE;

    public static synchronized LazySimpleSingleton getInstance() {
        if (LAZY_INSTANCE == null) {
            LAZY_INSTANCE = new LazySimpleSingleton();
        }
        return LAZY_INSTANCE;
    }
}

我们再来调试。当执行其中一个线程并调用 getlnstance0方法时,另一个线程在调用 getlnstancel方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到 RUNNING 状态继续调用 getlnstance0方法,如下图所示:

 上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是,用synchronized 加锁时,在线程数量比较多的情况下,如果 CPU分配压力上升,则会导致大批线程阻赛从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:

public class LazyDoubleSimpleSingleton {
    private LazyDoubleSimpleSingleton() {
    }

    private static LazyDoubleSimpleSingleton LAZY_INSTANCE;

    public static LazyDoubleSimpleSingleton getInstance() {
        // 双重检查锁
        if (LAZY_INSTANCE == null) {
            synchronized (LazyDoubleSimpleSingleton.class) {
                if (LAZY_INSTANCE == null) {
                    LAZY_INSTANCE = new LazyDoubleSimpleSingleton();
                }
            }
        }
        return LAZY_INSTANCE;
    }
}

然后进行断点调式:

         当第一个线程调用 getlnstance0 方法时,第二个线程也可以调用。当第一个线程执行到synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整个LazySimpleSingleton 类的阻塞,而是在 getlnstance0方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。
        但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然有。我们可以从举初始化的角度来考虑,看下面的代码,采用静态内部类的方式:

public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton() {
    }

    public static LazyStaticInnerClassSingleton getInstance() {
        return LazyHolder.LAZY_INSTANCE;
    }

    private static class LazyHolder {
        public static final LazyStaticInnerClassSingleton LAZY_INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,我们就不带大家一步一步调试了。但是,金无足赤,人无完人,单例模式亦如此。这种写法真的就完美了吗 ?

4.反射破坏单例

 现在我们来看一个事故现场。大家有没有发现,上面介绍的单例模式的构造方法除了加上 private关键字,没有做任何处理。如果我们使用反射来调用其构造方法,再调用 getlnstance0方法,应该有两个不同的实例。现在来看一段测试代码,以 LazylnnerClassSingleton 为例:

import Lazy.LazyStaticInnerClassSingleton;

import java.lang.reflect.Constructor;

/**
 * @author sj
 * @date 2022/12/8 15:58
 */
public class LazyStaticInnerClassSingletonTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = LazyStaticInnerClassSingleton.class;
            Constructor<?> constructor = clazz.getDeclaredConstructor(null);
            // 因为我们私有化了构造方法,所以需要暴力访问一波
            constructor.setAccessible(true);
            Object o = constructor.newInstance();
            System.out.println("第一次" + o);
            Object o2 = constructor.newInstance();
            System.out.println("第二次" + o2);
            System.out.println(o == o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行测试一波,看结果:

不难看出,这里创造了两个不同的实例,全局唯一并没有保证。那怎么办呢?我们来做一次优化。现在,我们在其构造方法中做些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:

/**
 * @author sj
 * @date 2022/12/8 15:47
 */
public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton() {
        if (LazyHolder.LAZY_INSTANCE != null) {
            throw new RuntimeException("不允许创造多个实例");

        }
    }

    public static LazyStaticInnerClassSingleton getInstance() {
        return LazyHolder.LAZY_INSTANCE;
    }

    private static class LazyHolder {
        public static final LazyStaticInnerClassSingleton LAZY_INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

再次运行代码,结果如下:

是不是开始觉得非常牛逼了,反正我是这么想的。但是,上面看似完美的单例写法还是有方法破坏,接下来我们来看看怎么被破坏。

5.序列化破坏单例 

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

package seriable;

import java.io.Serializable;

/**
 *
 //序列化
 //把内存中对象的状态转换为字节码的形式
 //把字节码通过IO输出流,写到磁盘上,永久保存下来,持久化

 //反序列化
 //将持久化的字节码内容,通过IO输入流读到内存中来,转化成一个Java对象

 */
class SerializableSingleton implements Serializable {
    
    public final static SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {
    }

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }

}

编写厕所代码:

import seriable.SerializableSingleton;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * @author sj
 * @date 2022/12/8 16:22
 */
public class SerializableSingletonTest {
    public static void main(String[] args) {
        SerializableSingleton s1 = null;
        SerializableSingleton s2 = SerializableSingleton.getInstance();

        FileOutputStream fos = null;
        try {

            fos = new FileOutputStream("SerializableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SerializableSingleton)ois.readObject();
            ois.close();

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

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行看看结果:

从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加 readResolve0方法即可。来看优化后的代码: 

package seriable;

import java.io.Serializable;

/**
 *
 //序列化
 //把内存中对象的状态转换为字节码的形式
 //把字节码通过IO输出流,写到磁盘上
 //永久保存下来,持久化

 //反序列化
 //将持久化的字节码内容,通过IO输入流读到内存中来
 //转化成一个Java对象

 */
public class SerializableSingleton implements Serializable {

    public final static SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {
    }

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }

}

再次运行:

 哇哦一致了,是不是很神奇,到底是为什么勒,有兴趣的朋友可以去看看JKD的源码,进入 ObjectlnputStream 类的 readObject0方法,进行查看

6.注册式单例

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

        1.枚举式单例

                编写代码如下:


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;
    }
}

        2.容器式单例     

                其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式单例模式的另一种写法,即容器式单例模式,创建 ContainerSingleton 类:  

 

public class ContainerSingleton {

    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className){
        Object instance = null;
        if(!ioc.containsKey(className)){
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            }catch (Exception e){
                e.printStackTrace();
            }
            return instance;
        }else{
            return ioc.get(className);
        }
    }

}

容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。到此,注册式单例模式介绍完毕。

 7.完结

         学习永无止境,只有不断学习,才能不断进步,成为优秀的自己,师承咕泡汤姆!

        大家可以想想容器式单例怎么保证线程安全哦!

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值