24

设计模式之单例设计模式

 4 years ago
source link: http://www.cnblogs.com/zhaoletian/p/12726365.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

饿汉式

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  (乔什布洛赫)提倡的方式,但是无法实现延时加载,那么我们究竟该如何在实际开发中选用呢,根据业务场景选用合适的单例模式,
没有最好的单例,只有最合适的单例.下一章我们将会聊一聊简单工厂模式和抽象工厂设计模式

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK