设计模式之单例模式

作者: 树獭非懒 | 来源:发表于2018-09-12 11:17 被阅读9次

单例模式是设计模式中相对是来说最容易理解的设计模式,但其实大多数人只是知道单例模式的一些简单场景的使用和实现。比如饿汉单例、懒汉单例等。实际上,在很多场景下,这些实现方式有很多问题。那么在不同场景下我们应该如何选择单例的实现呢。这次我们会由浅入深的学习,彻底的来认识一下单例模式。

一、单例模式的定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

二、UML类图

它的类图很简单,就是client使用这个单例类就行了。
需要注意的是单例类的构造方法是私有的。

单例UML.png

三、使用场景

它的使用场景也很明确。
确保某个类只要一个对象的场景,从而避免产生多个对象消耗过多的资源。

从定义、UML类图和使用场景都透露出两个字--简单。

可它的背后真的只是那么简单吗?哈哈,表明简单的东西背后往往是不简单的。

嗯哼,不信,那就先放一张图来震下场子

单例模式.png

这就是我这篇文章要说的核心内容了。

四、不同场景下的实现

1.说实现之前,我们先要看看实现单例模式的关键点

(1)构造方法不对外开放。这个很容易理解,如果开放了那么和话,外面随便new一下就实现了一个对象,还单例个毛线呢。
(2)确保单例类的对象只有一个,尤其是在多线程环境下。这个后面会详细介绍。
(3)确保单例类的对象在反序列化时不会重新构建对象。

那么下面我一一介绍各种实现方法和它们的优缺点

2.最简单的实现方法--饿汉单例

饿汉,顾名思义,非常饥饿的汉子,所以一定义这个对象就给实例化了。

public class HungerSingleton {

    private static HungerSingleton singleton=new HungerSingleton();
    private HungerSingleton() {}
    
    public static HungerSingleton getSingleton() {
        return singleton;
    }
}

实现非常简单,通过一个静态方法返回一个静态对象。

但是缺点也很明显,如果我不需要这个对象的话,它也会创建对象,浪费空间资源和影响时间性能。

3.给饥饿的汉子一张床--懒汉单例

给了饥饿的汉子一张床后会忘记饥饿,但它会变懒。也就不急着实例化对象了,就是所谓的懒加载。

public class LazySingleton {

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

仔细一看我们发现这个getSingleton方法加了个synchronize关键字。这个就是我们之前说的”保证在多线程下对象唯一“的方法。

咋一看很完美,但还是有一个很大的问题就是:

我们知道synchronize同步是比较消耗性能的,而且每次调用getSingleton方法都会进行同步,这回造成不必要的性能开销。

4.懒汉单例的优化--DCL单例

针对上面的懒汉单例的缺点,出现了DCL(Double Check Lock)模式。其实解决的方式很简单了,只需要在外层加一个判空就OK了。

这样就整个程序就有了两次判空,第一次的目的是为了避免多余的同步操作。第二次是为了对象实例化的唯一性。

public class DCLSingleton {

    private DCLSingleton(){};

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

这里我们考虑到了多线程的情景又考虑到性能的问题。看起来就完美了。其实不深入研究下去,这的确是完美的单例写法。那么还有什么不完美的地方呢?

就是这句话

singleton=new DCLSingleton();

这个语句他不是一个原子操作。

也就是说

它表明上只有一步其实有三步操作

(1)给DCLSingleton的实例分配内存

(2)调用Singleton()的构造方法,初始化成员字段

(3)将singleton对象指向分配的内存空间

由于java编译器允许处理器乱序执行,以及jdk1.5之前的java内存模型(JMM)中寄存器、cache到主内存回写顺序的规定。所以不能保证上面三步执行的顺序。很有可能会按照(1)-(3)-(2)的顺序
这就会有一个问题,高并发的情况下,当A线程执行到(3),被切换到了B线程,那么B线程就会直接取走了singleton对象,但是该对象因为没有执行第二步,就没有真正的实例化。所以就会被B拿去使用导致出错,发生DCL失效。

那肿么办呀?这个我们没法控制啊。

幸运的是,在jdk1.5的时候,出现了一个volatile关键字。当我们的jdk版本大于等于1.5的时候,只要在定义的时候加上这个volatile就能够解决了问题了。

private static volatile DCLSingleton singleton=null;

虽然有点小瑕疵但是DCL的优点还是多的,大多数情景下可以保证单例对象的唯一性。但如果你的并发场景复杂或者版本低于jdk1.5的时候,可能需要一个更好的实现方式了。

5.《java并发编程实践》推荐--静态内部类单例

java并发编程实践书中并不赞同DCL实现方式,因为这种方式还是会出现DCL失效。它建议使用静态内部类实现

public class InternalClassSingleton {
    private InternalClassSingleton(){};
    
    public static InternalClassSingleton getSingleton(){
        return SingletonHolder.singleton;
    }
    
    private static class SingletonHolder{
        private static final InternalClassSingleton singleton=new InternalClassSingleton();
    }

}

这种方式也是很简洁的,它只会在第一次调用getSingleton()的时候才会让singleton被初始化。因此第一次加载getSingleton的时候会导致虚拟机加载SingletonHolder类,这种方式不仅能够保证单例对象的唯一性,同时也延迟了单例的实例化。推荐使用的单例模式实现方式。

6.最简单的单例实现--枚举单例

我们都知道枚举和普通的java类是一样的,不仅有字段还有自己的方法。重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它只有一个单例。

public class EnumSingleton {
    private EnumSingleton(){};
    enum Enum{
        INSTANCE;
        public static Enum getInstance(){
            return INSTANCE;
        }
    }
}

实在是简单,看着还舒服,重点不需要考虑多线程的场景问题。美滋滋~~

除了上面说到,其实还有其他的方式实现,比如利用容器等等。但上面的方式足以应对各种的场景了,我们只需要根据不同的场景和自己的喜好来选择合适的就好了。

相关文章

网友评论

    本文标题:设计模式之单例模式

    本文链接:https://www.haomeiwen.com/subject/fqfvgftx.html