设计模式之单例设计模式

栏目: IT技术 · 发布时间: 4年前

内容简介:前面我们已经讲解了设计模式的七大设计原则,今天我们就来聊一聊设计模式中的单例设计模式,看看如何从小小单例模式衍生出来一个大世界。单例设计模式最简单的两个形态分为“懒汉式”和“饿汉式”,顾名思义,懒汉式就是基于事件驱动去加载,俗称懒加载,饿汉式就是提前加载。饿汉式

前面我们已经讲解了 设计模式 的七大设计原则,今天我们就来聊一聊设计模式中的单例设计模式,看看如何从小小单例模式衍生出来一个大世界。

单例设计模式最简单的两个形态分为“懒汉式”和“饿汉式”,顾名思义,懒汉式就是基于事件驱动去加载,俗称懒加载,饿汉式就是提前加载。

饿汉式

public class Singletion {

    private Singletion() {
    }

    private static Singletion singletion = new Singletion();

    public static Singletion getInstance() {
        return singletion;
    }
}

上面我们是通过静态变量的形式实例化一次,我们可不可以换一种下面的形式呢,我们把创建单例对象放在静态代码块中,可以实现相同的效果

public class Singletion {

    private Singletion() {
    }
    static{
        singletion = new Singletion();
    }

    private static Singletion singletion;

    public static Singletion getInstance() {
        return singletion;
    }
}

看完饿汉式我们再看下懒汉式

public class Singletion {

    private Singletion() {
    }
    private static Singletion singletion;

    public static Singletion getInstance() {
        if(singletion==null){
            return new Singletion();
        }
        return singletion;
    }
}

但是这种单例有一个很明显的问题,线程不安全,在多线程环境下,很有可能创建多个对象,我们需要改进一下,加锁,没错就是加锁

public class Singletion {

    private Singletion() {
    }
    private static Singletion singletion;

    public static synchronized Singletion getInstance() {
        if(singletion==null){
            return new Singletion();
        }
        return singletion;
    }
}

那么我把锁加在方法上怎么样呢,如果被一个外国人看到,我想他一定会说:噢,天哪,这真是个糟糕的决定.为什么说是个糟糕的决定呢?

在我们实际开发中,一个方法中一定有许多代码要执行,我们仅仅只是想同步创建单例对象这一部分代码,而我们却把锁加在了方法上.

这就好比一个厕所有多个坑位,我们只是想在每个坑位的门上加锁,而你却把锁加在了厕所门上,当进去一个人就把厕所锁上,这样显然不太合适,因此我就再次改造了一下

public class Singletion {

    private Singletion() {
    }

    private static Singletion singletion;

    public static Singletion getInstance() {

        if (singletion == null) {
            synchronized (Singletion.class) {
                if (singletion == null) {
                    return new Singletion();
                }
            }
        }
        return singletion;
    }
}

这样我们使用过加锁和双重检查机制解决了多线程不安全的问题,事情真的就万事大吉了?如果让一个外国人看到,我想他会说:噢,天哪,这真是个糟糕的决定.

这里我们就要聊一聊JVM.编译器和处理器的指令重 排序 和对象创建了

对象创建在我们new的时候到底做了些什么呢?

1:为对象分配内存空间

2:初始化对象

3:将对象指向分配的内存空间

而指令重排序做了一系列优化,对象创建的过程顺序很有可能1->3->2,那么在多线程环境下很有可能线程1执行了 1,3还没有执行2初始化对象,另一个线程调用getInstance方法发现单例对象不为null,直接返回单例对象,但是此时单例对象还没有执行2,也就是对象初始化.

为了解决指令重排序可能产生的影响,我们需要在本单例对象的静态变量上加上volitaile关键字修饰才能保证后续不会出问 题.

public class Singletion {

    private Singletion() {
    }

    private static volatile Singletion singletion;

    public static Singletion getInstance() {

        if (singletion == null) {
            synchronized (Singletion.class) {
                if (singletion == null) {
                    return new Singletion();
                }
            }
        }
        return singletion;
    }
}

那么这样就结束了?有没有办法让JVM帮助我们保证线程安全呢?毕竟我不想写太多代码,接下来我们聊一聊静态内部类

public class Singletion {

    private Singletion() {
    }

    private static class SingletonInside{
        private static final Singletion SGT=new Singletion();
    }

    public static Singletion getInstance() {
        return SingletonInside.SGT;
    }
}

才能保证后续不会出问 题.这种方式采用类加载的机制来保证线程安全,并且实现了懒加载,只有访问静态内部类才回去加载静态内部类.

这样就万无一失了?这时如果一个外国人看到,我想他会说:噢,天啊,这真是个糟糕的决定.

如果此刻我要用反射去创建对象,还能说万无一失吗,在反射的基础上,上面的单例全部都可以推翻?

  Constructor<Singletion> constructor = Singletion.class.getDeclaredConstructor();
   constructor.setAccessible(true);

        Singletion instance1 = constructor.newInstance();
        Singletion instance2 = constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);    

你会惊讶的发现,两次对象创建的地址不一样,创建了两个不一样地址的对象

那么如何才能防止反射创建呢?

public class Singletion {

    private Singletion() {
        if (SingletonInside.SGT != null) {
            throw new RuntimeException("不允许反射创建!");
        }
    }

    private static class SingletonInside{
        private static final Singletion SGT=new Singletion();
    }

    public static Singletion getInstance() {
        return SingletonInside.SGT;
    }
}

这样就可以了吗?如果实现了序列化接口,我们通过序列化和反序列化还能拿到同一个对象吗?

         Singletion instance = Singletion.getInstance();    
         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/main/resources/temp.txt"));
         oos.writeObject(instance1);
         oos.close();
         //反序列化
         ObjectInputStream ois = new ObjectInputStream(
         new FileInputStream("src/main/resources/temp.txt"));
         Singletion instance2 = (Singletion)ois.readObject();

我们通过比较地址会发现,序列化后的对象会创建另一个内存地址,我们如何防止序列化呢? readResolve方法序列化 ,我们就直接返回对象

public class Singletion implements Serializable {

    private Singletion() {
        if (SingletonInside.SGT != null) {
            throw new RuntimeException("不允许反射创建!");
        }
    }

    private static class SingletonInside{
        private static final Singletion SGT=new Singletion();
    }

    public static Singletion getInstance() {
        return SingletonInside.SGT;
    }

    private Object readResolve(){
        return SingletonInside.SGT;
    }
    
}

目前单例模式还有最后一种终级方案,可以解决上述问题

public enum Singleton implements Serializable{
    SGT;
    
    public void say(){
        System.out.println("我是枚举单例");
    }
}
这种方式是 Effective Java  作者 Josh Bloch  (乔什布洛赫)提倡的方式,但是无法实现延时加载,那么我们究竟该如何在实际开发中选用呢,根据业务场景选用合适的单例模式,
没有最好的单例,只有最合适的单例.下一章我们将会聊一聊简单工厂模式和抽象工厂设计模式

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

测试驱动开发的艺术

测试驱动开发的艺术

Lasse Koskela / 李贝 / 人民邮电出版社 / 20101023 / 59.00元

在传统的软件开发中,开发人员对于代码是否正确心中无底,一切依赖于后期的测试环节。极限编程反其道而行之,主张采用测试驱动开发(TDD)的方法,即通过测试定义所要开发的功能的接口,然后实现功能的开发过程。TDD通过不断地测试推动代码的开发,既简化了代码,又保证了软件质量。 本书采用“手把手”的教学方式,通过大量实例来解释TDD,还专门用几章的篇幅来讲解如何为难于测试的技术编写单元测试。全书内容循......一起来看看 《测试驱动开发的艺术》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器