Java单例模式

作者: SpaceCat | 来源:发表于2019-04-07 16:54 被阅读0次

    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。

    1、介绍

    数学与逻辑学中,singleton定义为“有且仅有一个元素的集合”。单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”

    1.1 定义

    Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
    单例模式有以下特点:

    • 单例类只能有一个实例。
    • 单例类必须自己自己创建自己的唯一实例。
    • 单例类必须给所有其他对象提供这一实例。

    1.2 应用场景和好处

    对于系统中的某些类来说,只有一个实例很重要。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
    单例模式能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

    2、单例模式的实现

    通常单例模式在Java语言中,有两种构建方式:

    • 饿汉方式。指全局的单例实例在类装载时构建。
    • 懒汉方式。指全局的单例实例在第一次被使用时构建。

    通常来说,单例模式实现的关键就是要阻止该类进行通常意义上的实例化。在Java中,通过将类的构造函数定义为private,保证该类不能进行一般意义上的实例化。除此之外,为了能够提供单例的全局访问,一般来说,会在该类中定义一个静态成员,并配套一个初始化和访问该静态实例成员的静态方法。

    2.1 饿汉方式

    饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
    这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。
    Singleton.java:

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class Singleton {
        private static Singleton instance = new Singleton();
        private Singleton(){}
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    SingletonTest.java:

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class SingletonTest {
        public static void main(String []args){
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = Singleton.getInstance();
            System.out.println(s1 == s2);
        }
    }
    

    运行上面的代码输出如下:

    true
    
    Process finished with exit code 0
    

    可见,两次访问Singleton.getInstance()方法,确实得到的是同一个实例。

    2.2 懒汉方式

    懒汉方式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。

    2.2.1 最简单的懒汉方式实现单例模式

    Singleton.java:

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance(){
            if(null == instance){
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    2.2.2 线程安全的单例模式实现

    上面单例模式的实现,并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,可能会导致创建多个实例,因此需要加锁解决线程同步问题。
    Singleton.java:

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){}
        public static synchronized Singleton getInstance(){
            if(null == instance){
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    这样,就解决了线程安全的问题。但是,锁是有开销的。这里,我们的本意是像通过锁来控制单例类中的成员只被初始化一次,但在上面的实现中,整个的getInstance()方法都不能被并发调用。这并不是我们想要的。

    2.2.3 双重校验锁

    加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁。
    Singleton.java:

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance(){
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    上面在同步代码块外多了一层instance为空的判断。由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加第二个if (instance == null)语句。

    该方案既实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题。但是,它并非没有问题。
    这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
    这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
    以上就是双重校验锁会失效的原因,不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。
    Singleton.java:

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class Singleton {
        private static volatile Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance(){
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    到这里,这个实现是最常见的单例模式的实现。百度百科上贴出的也是这个版本。

    2.3.4 内部类实现

    /**
     * Created by chengxia on 2019/4/7.
     */
    public class Singleton {
        private static class SingletonHolder{
            public static Singleton instance = new Singleton();
        }
        private Singleton(){}
        public static Singleton getInstance(){
            return SingletonHolder.instance;
        }
    }
    

    该方式利用了类加载机制来保证只创建一个instance实例,与饿汉方式一样,不存在多线程并发的问题。它在内部类里面去创建对象实例。这样,只要应用中不使用内部类(也就是不调用getInstance方法),JVM就不会去加载这个单例类,也就不会创建单例对象,这样便实现懒汉式的延迟加载。也就是说,这种方式可以同时保证延迟加载和线程安全。
    除了上面的实现外,还有通过枚举类来实现单例模式的。据说,那还是一种比较好的实现方式。但是代码中,比较少见,这里暂且不做讨论。

    参考资料

    相关文章

      网友评论

        本文标题:Java单例模式

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