单例模式的几种实现方式

Posted Java高知社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的几种实现方式相关的知识,希望对你有一定的参考价值。

1.单例模式简述

单例模式顾名思义就是"一个实例"的设计模式,需要使用设计模式的思路达成调用类的时候都是同一个实例。那么自然的构造函数肯定不能随意调用了,那肯定是私有的。但是肯定要有接口让外部调用啊,所以得提供一个接口,这个接口呢,返回的每一个实例都应该是一样的。


2.单例模式的主要实现方式

单例模式刚接触的时候以为只有懒汉式和饥汉式,随着了解的逐步深入,了解了更多优雅更全面的写法,需要兼顾线程安全。


2.1 饥汉式(饿汉式)

package com.example.test.demo;


public class JHanSingleton {

    //static final单例对象,类加载的时候就初始化

    private static final JHanSingleton instance = new JHanSingleton();


    //私有构造方法,使得外界不能直接new

    private JHanSingleton() {

    }


    //公有静态方法,对外提供获取单例接口

    public static JHanSingleton getInstance() {

        return instance;

    }

}


饥汉式解决了多线程并发的问题,因为在加载这个类的时候,就实例化了instance。当getInstatnce方法被调用时,得到的永远是类加载时初始化的对象(反序列化的情况除外)。但这也带来了另一个问题,如果有大量的类都采用了饥汉式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能。

2.2 懒汉式

package com.example.test.demo;


public class LanHanSingleton {

    private static LanHanSingleton instance;


    private LanHanSingleton() {


    }


    /**

     * 增加synchronized关键字,该方法为同步方法,保证多线程单例对象唯一

     */

    public static synchronized LanHanSingleton getInstance() {

        if (instance == null) {

            instance = new LanHanSingleton();

        }

        return instance;

    }

}


可以注意到getInstance方法前加了synchronized 关键字,让getInstance方法成为同步方法,这样就保证了当getInstance第一次被调用,即instance被实例化时,别的调用不会进入到该方法,保证了多线程中单例对象的唯一性。

优点:单例对象在第一次调用才被实例化,有效节省内存,并保证了线程安全。

缺点:同步是针对方法的,以后每次调用getInstance时(就算intance已经被实例化了),也会进行同步,造成了不必要的同步开销。


2.3 Double CheckLock(DCL)单例模式

package com.example.test.demo;


public class DCLSingleton {

    //增加volatile关键字,确保实例化instance时,编译成汇编指令的执行顺序

    private volatile static DCLSingleton instance;


    private DCLSingleton() {


    }


    public static DCLSingleton getInstance() {

        if (instance == null) {

            synchronized (DCLSingleton.class) {

                //当第一次调用getInstance方法时,即instance为空时,同步操作,保证多线程实例唯一

                //当以后调用getInstance方法时,即instance不为空时,不进入同步代码块,减少了不必要的同步开销

                if (instance == null) {

                    instance = new DCLSingleton();

                }

            }

        }

        return instance;

    }

}

如果instance不加volatile的话DCL失效:

在JDK1.5之前,可能会有DCL实现的问题,上述代码中的如下代码,在Java里虽然是一句代码,但它并不是一个真正的原子操作。

instance = new DCLSingleton();


它编译成最终的汇编指令,会有下面3个阶段:

  1. 给DCLSingleton实例分配内存

  2. 调用DCLSingleton的构造函数,初始化成员变量。

  3. 将instance指向分配的内存空间(这个操作以后,instance才不为null)

在jdk1.5之前,上述的2、3步骤不能保证顺序,也就是说有可能是1-2-3,也有可能是1-3-2。如果是1-3-2,当线程A执行完步骤3(instance已经不为null),但是还没执行完2,线程B又调用了getInstance方法,这时候线程B所得到的就是线程A没有执行步骤2(没有执行完构造函数)的instance,线程B在使用这样的instance时,就有可能会出错。这就是DCL失效。也叫指令重排。

知识点:什么是指令重排?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。


在jdk1.5之后,可以使用volatile关键字,保证汇编指令的执行顺序,虽然会影响性能,但是和程序的正确性比起来,可以忽悠不计。这里用一句话概况就是volatile会禁止指令重排。(volatile另一个作用是保证可见性)


优点:第一次执行getInstance时instance才被实例化,节省内存;多线程情况下,基本安全;并且在instance实例化以后,再次调用getInstance时,不会有同步消耗。

缺点:jdk1.5以前,有可能DCL失效,不过现在一般都会用比较高的版本,这个问题不大;Java内存模型影响导致失效;jdk1.5以后,使用volatile关键字,虽然能解决DCL失效问题,但是会影响部分性能。


2.4 静态内部类单例模式

package com.example.test.demo;


public class StaticClassSingleton {

    //私有的构造方法,防止new

    private StaticClassSingleton() {


    }


    public static StaticClassSingleton getInstance() {

        return StaticClassSingletonHolder.instance;

    }


    /**

     * 静态内部类

     */

    private static class StaticClassSingletonHolder {

        //第一次加载内部类的时候,实例化单例对象

        private static final StaticClassSingleton instance = new StaticClassSingleton();

    }

}


第一次加载StaticClassSingleton类时,并不会实例化instance,只有第一次调用getInstance方法时,Java虚拟机才会去加载StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的。这是推荐使用的一种单例模式。

这个我不是很明白,为什么静态内部类里面的实例变量不会被加载,我写个demo验证一下正确性,至于为什么估计涉及到JVM 类的加载=-=

demo:

package com.example.test.demo;


public class StaticClassSingleton {

    //私有的构造方法,防止new

    private StaticClassSingleton() {


    }


    public static StaticClassSingleton getInstance() {

        System.out.println("StaticClassSingleton getInstance");

        return StaticClassSingletonHolder.instance;

    }


    public static void test(){

        System.out.println("StaticClassSingleton test");

    }


    /**

     * 静态内部类

     */

    private static class StaticClassSingletonHolder {

        //第一次加载内部类的时候,实例化单例对象

        private static final StaticClassSingleton instance = new StaticClassSingleton();

        static {

            System.out.println("StaticClassSingletonHolder load static field");

        }

    }

}


package com.example.test;

import com.example.test.demo.StaticClassSingleton;


public class SingletonTest {

    public static void main(String[] args){

        StaticClassSingleton.test();

        StaticClassSingleton.getInstance();

    }

}


执行结果:

StaticClassSingleton test

StaticClassSingleton getInstance

StaticClassSingletonHolder load static field


Process finished with exit code 0


恩,从结果来看是对的。我这个demo的依据是静态代码块和静态成员变量都只会加载一次,所以如果是紧接着getInstance后面加载那上面说的没错。相应的如果我多调用一个getInstance,那static代码块也不会再次加载了。

StaticClassSingleton test

StaticClassSingleton getInstance

StaticClassSingletonHolder load static field

StaticClassSingleton getInstance


2.5 枚举单例模式

package com.example.demo_11_singleton.demo;


public enum EnumSingleton {

    //枚举实例的创建是线程安全的,任何情况下都是单例(包括反序列化)

    INSTANCE;


    public void doSomething(){


    }

}


枚举不仅有字段还能有自己的方法,并且枚举实例创建是线程安全的,就算反序列化时,也不会创建新的实例。

----------

除了枚举模式以外,其他实现方式,在反序列化时都会创建新的对象。


为了防止对象在反序列化时创建新的对象,需要加上如下方法:

private Object readResole() throws ObjectStreamException {

      return instance;

}

这是一个钩子函数,在反序列化创建对象时会调用它,我们直接返回instance就是说,不要按照默认那样去创建新的对象,而是直接将instance返回。


3.总结

看了这么多写法,其实懒汉式和饥汉式已经被后面更优雅的实现方式比下去了,看起来用double check、静态内部类或者枚举会更好一点。



长按二维码识别关注Java高知社区,学习更多高知知识,点赞+转发


以上是关于单例模式的几种实现方式的主要内容,如果未能解决你的问题,请参考以下文章

单例模式的几种实现方式

JAVA单例模式的几种实现方法

单例模式的几种实现方式

关于单利模式的几种实现方式

单例模式的几种写法

单例模式的几种写法