【设计模式】三、单例模式(10分钟深度搞定)

前言

单例模式是保证任何情况下,都仅有一个实例,并提供全局访问的方法。

一、饿汉式

先上代码

/**
 * 饿汉式
 */
public class Singleton1 {

    private static final Singleton1 instance = new Singleton1();

    /**
     * 私有化构造方法 (防止手动new)
     */
    private Singleton1(){}

    public static Singleton1 getInstance(){
        return instance;
    }
}

饿汉式的关键在于instance作为类变量直接得到初始化,该方法能够百分之百的保证同步,也就是说instance在多线程下也不可能被实例化两次,但是instance被ClassLoader加载后可能很长时间才会被使用,那就意味着instance实例所开辟的空间的堆内存会驻留更久的时间。
如果一个类中的成员属性比较少,所占用的内存资源不多,饿汉式也未尝不可。总结起来,饿汉式可以保证多线程下唯一的实例,getInstance性能也比较高,但是无法进行懒加载

二、懒汉式

/**
 * 懒汉式
 */
public class Singleton2 {

    private static  Singleton2 instance = null;

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

    // 存在线程安全问题
    public static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

Singleton2 的类变量instance = null,当Singleton2.class被初始化的时候instance并不会被实例化,在getInstance方法中会判断instance 实例是否被实例化,看起来没什么问题,但在多线程环境下,会导致instance可能被实例化多次。 线程1判断null == instance为true时,还没有实例化instance,切换到了线程2运行,线程2判断null == instance也为true。就会实例化多次。

三、懒汉式 + 同步

// 解决线程安全问题,但是效率低
    public synchronized static Singleton2 getInstance2() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }

采用懒汉式 + 数据同步方式既满足了懒加载又能百分之百保证instance实例的唯一性,但是synchronized 关键字天生的排他性导致了getInstance方法只能在同一时刻被一个线程所访问,性能低下。

四、懒汉式 + Double-Check

// 解决线程安全问题,提升效率
    private static  Singleton2 instance = null;
    private String msg;
    
    /**
     * 私有化构造方法
     */
    private Singleton2() {
        msg = "初始化参数";
    }
    public static Singleton2 getInstance3() {
        if (instance == null) {
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

当两个线程发现null == instance成立时,只有一个线程有资格进入同步代码块,完成对instance的实例化,随后的线程发现 null == instance 不成立则无须进行任何操作,以后对getInstance的访问就不需要数据同步的保护了。

这种方式看起来那么的完美,既满足了懒加载,有保证instance实例的唯一性。Double-Check的方式提供了高效的数据同步策略,可以允许多个线程同时对getInstance进行访问。但是这种方式有可能引起空指针异常,我们分析一下。

Singleton4的构造函数中,初始化了msg还有Singleton2自身,根据JVM指令重排序和Happens-Before规则,这两者之间的实例化顺序并无前后关系的约束,那么极有可能instance最先被实例化,而msg并未完成实例化,未完成初始化的实例调用其他方法将会抛出空指针异常。

五、Volatile + Double + Check

 private volatile static  Singleton2 instance = null;
 private String msg;
    
    /**
     * 私有化构造方法
     */
    private Singleton2() {
        msg = "初始化参数";
    }
    public static Singleton2 getInstance3() {
        if (instance == null) {
            synchronized (Singleton2.class){
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

在instance前 加上 volatile的关键字,则可以防止重排序的发生。但终归加了synchronized,对性能依旧造成了影响。有没有更好的方式呢?有!

六、Holder方式

public class Singleton3 {

    private static Singleton3 instance = null;

    private static class Holder{
        private static final Singleton3 singleton3 = new Singleton3();
    }


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

    public static final Singleton3 getInstance() {
        return Holder.singleton3;
    }
    
}

在Singleton6中并没有instance的静态变量,而是将其放在静态内部类Holder类中,因此Singleton3初始化过程中并不会创建Singleton3的实例,Holder类中定义了Singleton3的静态变量,并且直接进行了实例化,当Holder被直接引用的时候则会创建Singleton3的实例,该方法又是同步方法,保证了内存的可见性,JVM的顺序性和原子性。Holder方式是单例设计最好的设计之一。但是!依然优缺点。

反射破坏单例

public static void main(String[] args) throws Exception {
        // 无聊情况下进行破坏
        Class<?> clazz = Singleton3.class;
        // 获取私有化构造方法
        Constructor constructor = clazz.getDeclaredConstructor(null);
        // 强制访问
        constructor.setAccessible(true);
        // 暴力初始化两次
        Object o1 = constructor.newInstance();
        Object o2 = constructor.newInstance();
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1 == o2);
    }

运行结果
在这里插入图片描述
显然我们创建出来了两个实例。破坏了我们的初衷。我们来优化一次,在构造方法做限制,一旦重复创建,我们就抛异常。

public class Singleton3 {

    private static class Holder{
        private static final Singleton3 singleton3 = new Singleton3();
    }

    /**
     * 私有化构造方法
     */
    private Singleton3() {
        if(Holder.singleton3 != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static final Singleton3 getInstance() {
        return Holder.singleton3;
    }

}

感觉上该单例已经完美了,然而还有可能被破坏。

序列化破坏单例

实现序列化

public class Singleton4 implements Serializable {

    private static class Holder{
        private static final Singleton4 singleton3 = new Singleton4();
    }

    /**
     * 私有化构造方法
     */
    private Singleton4() {
        if(Holder.singleton3 != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static final Singleton4 getInstance() {
        return Holder.singleton3;
    }

}

测试代码

		Singleton4 s1 = null;
        Singleton4 s2 = Singleton4.getInstance();

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

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

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

运行结果
在这里插入图片描述
显然,单例又遭到破坏。如何解决呢?只需要添加readResolve 方法即可。

public class Singleton4 implements Serializable {

    private static class Holder{
        private static final Singleton4 singleton3 = new Singleton4();
    }

    /**
     * 私有化构造方法
     */
    private Singleton4() {
        if(Holder.singleton3 != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static final Singleton4 getInstance() {
        return Holder.singleton3;
    }

    private Object readResolve(){
        return Holder.singleton3;
    }

}

再看运行效果
在这里插入图片描述
有兴趣的同学可以查看JDK的源码,发现实际上,这里我们还是实例化了两次,只不过第二次创建的对象没有被返回而已。这样也会造成内存的不必要浪费。

七、注册式单例

注册时单例就是将每个实例登记到某个地方,使用唯一标识来获取实例。

枚举式单例

public enum Singleton5  {

    INSTANCE;
    private Object data;


    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static final Singleton5 getInstance() {
        return INSTANCE;
    }
}

惊喜的是,枚举类天生就防止反射破坏与序列化破坏,有兴趣的同学,可以查阅JDK源码。

容器式单例

枚举类虽然写法优雅,但是在类加载之时,就将所有对象初始化放在内存中,这其实与饿汉式无异。容器式则是将Bean 放在 concurrentHashMap<String,Object>中,详细可参照Spring IOC的实现,这里就不多做叙述了。

源码地址: https://gitee.com/xiaowangz/learning-note.git

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值