美文网首页
单例模式(Singleton Pattern)

单例模式(Singleton Pattern)

作者: lxbnjupt | 来源:发表于2018-07-31 17:30 被阅读0次

    一、单例模式简介

    1. 定义

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,该设计模式属于创建型模式。这种设计模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    2. 特点

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

    3. 实现关键

    • 构造函数是私有的;
    • 一个静态的变量用来保存单实例的引用;
    • 一个公有的静态方法用来获取单实例的引用,如果实例为null即创建一个。

    二、单例模式实现

    单例模式的实现有很多种方式,根据需求场景大致可以分为2大类。其中一类是初始化单例类时就创建单实例,另一类是延迟创建单实例(即使用时才创建,亦即懒加载)。

    1. 初始化单例类时创建单实例

    (1)饿汉式

    该创建方式基于JVM的类加载机制,保证单实例只会被创建一次,从而避免了多线程的同步问题,是线程安全的。其优点是实现方式简单,缺点是一旦类被加载,单实例就会初始化,没有实现懒加载。

    • JVM在类的初始化阶段,即在Class被加载后、被线程使用前,会执行类的初始化;
    • 在执行类的初始化期间,JVM会去获取一个锁,可以同步多个线程对同一个类的初始化。
    public class Singleton {
    
        // 静态变量保存单实例的引用
        private static Singleton INSTANCE = new Singleton();
    
        // 构造函数私有
        private Singleton() {
    
        }
    
        // 公有的静态方法用来获取单实例的引用
        public static Singleton getInstance() {
            return INSTANCE;
        }
    }
    

    (2)枚举

    该创建方式利用的枚举的特性实现单例,由JVM保证线程安全,是最简洁、易用的单例实现方式。
    《Effective Java》:单元素的枚举类型已经成为实现 Singleton 的最佳方法。

    public enum Singleton {
        // 定义一个枚举的元素,即为单例类的一个实例
        INSTANCE;
    }
    

    2. 延迟创建单实例(懒加载)

    (1)懒汉式

    该创建方式的特点是需要单例时才创建单实例,即实现懒加载。但是,这种创建方式会导致一个问题,那就是线程不安全。当多个线程并发调用getInstance方法时,可能会创建多个实例,从而导致单例模式失效,因此需要对其进行改进和优化。

    public class Singleton {
    
        // 类加载时先不创建单例对象,单实例引用置为null
        private static Singleton INSTANCE = null;
    
        // 构造函数私有
        private Singleton() {
    
        }
    
        // 需要单例时才创建单实例
        public static Singleton getInstance() {
            // 先判断单实例是否为空,以避免重复创建
            if (INSTANCE == null) {
                INSTANCE = new Singleton();
            }
            return INSTANCE;
        }
    }
    

    (2)同步锁(懒汉式改进)

    该创建方式是对懒汉式的改进,通过使用同步锁(synchronized)机制创建单实例方法 ,避免多个线程同时调用造成单例被多次创建,从而达到线程安全的目的。但是,这种同步锁的方法又会带来性能问题,即同一时间内只有一个线程能够调用getInstance方法,其它线程则会因为被阻塞而一直等待,加锁导致耗费时间和性能,因此还需对同步锁进行再次优化。

    public class Singleton {
    
        private static Singleton INSTANCE = null;
    
        private Singleton() {
    
        }
    
        public static Singleton getInstance() {
            // 加入同步锁
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
            return INSTANCE;
        }
    }
    

    (3)双重校验锁(同步锁改进)

    该创建方式是对同步锁的改进,即在同步锁的基础上,再添加一层if判断若单例已创建,则不需再执行加锁操作就可获取实例,从而提高性能。执行两次检测很有必要的,当多线程调用时,如果多个线程同时执行完了第一次检查判断,其中一个进入同步代码块创建了实例,后面的线程因第二次检测判断就不会再创建新实例。

    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 = new Singleton()并非是一个原子操作。
    事实上,INSTANCE = new Singleton()这句话在JVM中大概做了下面 3 件事情:
    1.给 INSTANCE 分配内存;
    2.调用 Singleton 的构造函数来初始化成员变量;
    3.将 INSTANCE 对象指向分配的内存空间,执行完这步 INSTANCE 就不是null了。
    JVM的即时编译器中存在指令重排序的优化,也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 INSTANCE 已经不是null了(但却没有初始化),所以线程二会直接返回 INSTANCE,然后使用,接着报错。
    解决方法很简单,我们只需要将 INSTANCE 变量声明成 volatile 就可以了,大概可以改成像下面的样子:

    public class Singleton {
    
        // 声明成 volatile
        private volatile static Singleton INSTANCE;
    
        private Singleton() {
    
        }
    
        public static Singleton getInstance() {
            if (INSTANCE == null) {
                // 加入同步锁
                synchronized (Singleton.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new Singleton();
                    }
                }
            }
            return INSTANCE;
        }
    }
    

    (4)静态内部类

    该创建方式使用静态内部类来创建单实例,实现了懒加载及线程安全。当Singleton被加载时,其内部类并不会被初始化,从而不会创建单实例。只有getInstance() 方法被调用时,静态内部类被加载,此时才会去创建单实例,从而实现了懒加载。同时,基于JVM的类加载机制,保证单实例只会被创建一次,从而避免了多线程的同步问题,是线程安全的。

    public class Singleton {
    
        // 构造方法私有
        private Singleton() {
    
        }
    
        // 静态内部类
        private static class SingletonHolder {
            // 在静态内部类中创建单实例
            private static Singleton INSTANCE = new Singleton();
    
        }
    
        // 需要单例时才创建单实例
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    相关文章

      网友评论

          本文标题:单例模式(Singleton Pattern)

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