美文网首页
如何正确地写出单例模式

如何正确地写出单例模式

作者: 云枫随笔 | 来源:发表于2020-03-29 23:22 被阅读0次

    什么是单例模式

    一个类在JVM只有一个实例,并且提供一个全局访问入口。单例模式适用无状态的工具类,比如日志工具、字符串工具;
    还有全局信息类,比如全局计数、环境变量;在Java中如下类库是适用单例模式:

    • java.lang.Runtime#getRuntime();
    • java.awt.Desktop#getDesktop();
    • java.lang.System#getSecurityManager();

    单例模式的作用:节省内存;节省计算;结果的正确,比如全局计数器;方便管理。其实现方式很多,但不管何种实现方式,共同点:

    • 私有的构造函数;
    • 私有静态类对象;
    • 公有静态方法,唯一一个访问私有静态对象实例的方法。

    单例模式实现方式

    饿汉式或静态代码模块式

    public class EagerSingleton {
        private static final EagerSingleton instance = new EagerSingleton();
    
        private EagerSingleton() {
        }
        public static EagerSingleton getInstance() {
            return instance;
        }
    }
    

    另外一种变种的写法是

    /**
     * 饿汉式的变种,静态代码形式
     */
    public class StaticBlockSingleton {
        private static final StaticBlockSingleton singleton;
    
        private StaticBlockSingleton() {}
    
        static {
            singleton = new StaticBlockSingleton();
        }
        public StaticBlockSingleton getInstance() {
            return singleton;
        }
    }
    

    懒加载模式

    • 线程不安全的懒加载模式
    /**
     * 懒汉式:只适合单线程模式
     */
    public class LazySingleton {
        private static LazySingleton singleton;
    
        private LazySingleton() {}
    
        public LazySingleton getInstatnce(){
            if (null == singleton) {
                singleton = new LazySingleton();
            }
            return singleton;
        }
    }
    
    • 线程安全的懒加载模式
    /**
     * @description: 线程安全的懒加载单例模式
     * @author: agentzhu
     */
    public class ThreadSafeLazySingleton {
        private static ThreadSafeLazySingleton singleton;
    
        private ThreadSafeLazySingleton() {}
    
        /**
         * 加锁粒度大,多线程环境不能同时访问,并发效率低
         * @return
         */
        public synchronized ThreadSafeLazySingleton getInstatnce(){
            if (null == singleton) {
                singleton = new ThreadSafeLazySingleton();
            }
            return singleton;
        }
    }
    

    双重检验锁模式(double checked locking pattern)

    双重检验锁模式也是一种懒加载模式,是一种对安全型懒加载模式的优化,具体如下:

    public class Singleton {
        // 关键点1:声明成 volatile,禁止指令重排序
        private volatile static Singleton instance;
        private Singleton (){}
    
        public static Singleton getSingleton() {
            // 关键点2: 提高并发性
            if (instance == null) {
                synchronized (Singleton.class) {
                    // 关键点3:防止创建多个实例
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    volatile关键字的原因,new Singleton()JVM的实现三个不步骤,如下图:

    20200328_singleton_1.jpg

    由于指令重排,在多线程环境中易于引起使用未初始化完全的的对象,比如下图:

    20200328_singleton_2.jpg

    所以使用volatile关键字,其有两个特性:一个是可见性;另外一个是禁止指令重排序优化。而禁止指令重排序优化具体来说,在volatile变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完1-2-3 之后或者1-3-2之后,不存在执行到1-3然后取到值的情况。

    静态内部类 static nested class

    Java 5 以前的版本使用volatile的双检锁还是有问题的。其原因是Java 5以前的JMM(Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。所以Bill Pugh提供了一种静态内部类实现方式。

    public class Singleton {  
        private static class SingletonHelper {  
            private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
    
        public static final Singleton getInstance() {  
            return SingletonHelper.INSTANCE;
        }  
    }
    

    枚举式

    上面单例模式的实现方式都存在两个问题:

    1. 序列化与反序列化创建多个实例;
    2. 反射,创建多个实例;

    序列化与反序列化创建多个实例问题

    public class DoubleCheckSingleton implements Serializable {
        private static final long serialVersionUID = -7975945444590877513L;
    
        // 不用volatile 修饰,会出现不完全初始化的状态的实例
        private static volatile DoubleCheckSingleton singleton;
    
        private DoubleCheckSingleton() {}
    
        public static DoubleCheckSingleton getInstance() {
            if (null == singleton) { //关键点1: 提高并发效率
                synchronized (DoubleCheckSingleton.class) {
                    if (null == singleton) { // 关键点2:防止创建多个实例
                        // 存在三个步骤,顺利不是固定的[编译器的重排序优化],可能是:1,2,3;1,3,2
                        // 1.给singleton分配内存空间
                        // 2.调用Singleton的构造函数等来初始化singleton
                        // 3.将singleton对象指向分配的内存空间(执行完此步singleton就不是null了)
                        singleton = new DoubleCheckSingleton();
                    }
                }
            }
            return singleton;
        }
    
        private int value = 10;
    
        public int getValue() {
            return value;
        }
    
        public void setValue(int value) {
            this.value = value;
        }
    }
    

    测试结果:

    20
    10
    

    为此在DoubleCheckSingleton需要重写readResolve,它会在反序列化的时候被调用,所以我们可以在此方法中返回已有的对象实例。

    protected Object readResolve() {
        return singleton;
    }
    

    测试结果

    20
    20
    

    反射创建多个实例

    创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

    public class SingletonReflectionDemo {
        public static void main(String[] args) throws Exception {
            DoubleCheckSingleton singleton = DoubleCheckSingleton.getInstance();
            Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
            constructor.setAccessible(true);
            DoubleCheckSingleton singleton2 = (DoubleCheckSingleton) constructor.newInstance();
            if (singleton == singleton2) {
                System.out.println("Two objects are same");
            } else {
                System.out.println("Two objects are not same");
            }
            singleton.setValue(1);
            singleton2.setValue(2);
            System.out.println(singleton.getValue());
            System.out.println(singleton2.getValue());
        }
    }
    

    测试结果

    Two objects are not same
    1
    2
    

    我们再来看看枚举方式的单例实现方式,虽然在加载类的时候实例化,并且只有一个实例对象。存在的问题达不到懒加载的作用的。但是绝对解决上述提到的两个问题。首先,我们来看看如何使用枚举的方式来实现单例模式,然后再一一测试。

    public enum EnumSingleton {
        INSTANCE;
        int value;
    
        public int getValue() {
            return value;
        }
    
        public void setValue(int value) {
            this.value = value;
        }
    }
    
    • 验证序列化与反序列化创建多个实例的问题
    public class EnumSingletonSerializeDemo {
        private static EnumSingleton instanceOne = EnumSingleton.INSTANCE;
    
        public static void main(String[] args) {
            try {
                // Serialize to a file
                ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                        "filename.ser"));
                out.writeObject(instanceOne);
                out.close();
                instanceOne.setValue(20);
    
                // deserialize from a file
                ObjectInput in = new ObjectInputStream(new FileInputStream(
                        "filename.ser"));
                EnumSingleton instanceTwo = (EnumSingleton) in.readObject();
                in.close();
    
                System.out.println(instanceOne.getValue());
                System.out.println(instanceTwo.getValue());
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 验证反射创建多个实例问题

    反射进行创建枚举类的会直接报错,无法创建的。原因:枚举被设计成是单例模式,即枚举类型会由JVM在加载的时候,实例化枚举对象,你在枚举类中定义了多少个就会实例化多少个,JVM为了保证每一个枚举类元素的唯一实例,是不会允许外部进行new的,所以会把构造函数设计成private,防止用户生成实例,破坏唯一性。

    An enum type has no instances other than those defined by its enum constants. It is a compile-time error to attempt to explicitly instantiate an enum type. The final clone method in Enum ensures that enum constants can never be cloned, and the special treatment by the serialization mechanism ensures that duplicate instances are never created as a result of deserialization. Reflective instantiation of enum types is prohibited. Together, these four things ensure that no instances of an enum type exist beyond those defined by the enum constants.

    总结

    实现方式 优点 缺点
    饿汉式/静态代码模块式 简单,在类加载的时完成实例化;无线程同步问题 不使用此实例,也会在类加载的时候完成实例化,浪费内存;存在序列化、反射创建多个实例问题
    懒加载或者线程安全的懒加载式 获取实例的时候才初始化,但只适合单线程情况下使用 线程不安全或者并发度低;存在序列化、反射创建多个实例问题
    双重检验锁模式 线程安全;懒加载; 代码复杂度高,易写错;存在序列化、反射创建多个实例问题
    枚举式 线程安全;代码简单,流行度不高 不是懒加载,不存在序列化,反射创建多个实例问题

    参考资料

    相关文章

      网友评论

          本文标题:如何正确地写出单例模式

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