美文网首页
1 单例模式

1 单例模式

作者: 6cc89d7ec09f | 来源:发表于2018-10-10 18:25 被阅读17次

    1 单例模式的优点:


    1 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
    2 避免对资源的多重占用(比如写文件操作)。

    2 饿汉式的写法 缺点 以及解决办法


    //code 1
    public class Singleton {
        //在类内部实例化一个实例
        private static Singleton instance = new Singleton();
        //私有的构造函数,外部无法访问
        private Singleton() {
        }
        //对外提供获取实例的静态方法
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    //code 3 饿汉式变种
    public class Singleton2 {
        //在类内部定义
        private static Singleton2 instance;
        static {
            //实例化该实例
            instance = new Singleton2();
        }
        //私有的构造函数,外部无法访问
        private Singleton2() {
        }
        //对外提供获取实例的静态方法
        public static Singleton2 getInstance() {
            return instance;
        }
    }
    
    • 优点

    1 第一次使用时就已经被初始化了.当需要到数仓查询一些数据到内存中时,希望项目启动的时候就可以把数据加载完成,而且数仓查询缓慢,懒汉式会导致第一次查询耗时很长.
    2 该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。

    • 缺点

    1 不能延迟加载。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。
    2 如果这个类被多次加载的话也会造成多次实例化。

    • 解决办法

    1 通过静态内部类的方法
    2 懒汉式(下文有介绍)

    //code 4
    public class Singleton{
        //在静态内部类中初始化实例对象
        private static class SingletonHolder {
            private static final SingletonINSTANCE = new Singleton();
        }
        //私有的构造方法
        private Singleton() {
        }
        //对外提供获取实例的静态方法
        public static final SingletongetInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式更加合理。

    3 懒汉式的写法 缺点 以及解决办法


    //code 5
    public class Singleton {
        //定义实例
        private static Singleton instance;
        //私有构造方法
        private Singleton(){}
        //对外提供获取实例的静态方法
        public static Singleton getInstance() {
            //在对象被使用的时候才实例化
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
    • 优点

    1 实例化延迟到第一次被引用的时候。防止不必要的浪费

    • 缺点

    1 在多线程情况下,有可能两个线程同时进入if语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。

    • 解决办法1-同步
    //code 6
    public class SynchronizedSingleton {
        //定义实例
        private static SynchronizedSingleton instance;
        //私有构造方法
        private SynchronizedSingleton(){}
        //对外提供获取实例的静态方法,对该方法加锁
        public static synchronized SynchronizedSingleton getInstance() {
            //在对象被使用的时候才实例化
            if (instance == null) {
                instance = new SynchronizedSingleton();
            }
            return instance;
        }
    }
    

    1 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的延迟加载,
    2 效率很低,因为99%情况下不需要同步。(因为上面的synchronized的加锁范围是整个方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if语句中的情况,根本不需要同步操作,可以直接返回instance。)

    • 解决办法2-双重检测
      那么如何缩小锁的范围呢?相比于同步方法,同步代码块的加锁范围更小
    //code 7
    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {
        }
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    1 通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。

    2 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
    由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。(指令重排)
    线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有传到B使用的内存(缓存一致性)),程序很可能会崩溃。

    • 解决方法3 - volatile
      在J2SE 5.0中,这一问题被修正了。volatile关键字保证多个线程可以正确处理单件实例
    //code 8
    public class VolatileSingleton {
        private static volatile VolatileSingleton singleton;
    
        private VolatileSingleton() {
        }
    
        public static VolatileSingleton getSingleton() {
            if (singleton == null) {
                synchronized (VolatileSingleton.class) {
                    if (singleton == null) {
                        singleton = new VolatileSingleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    1 上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。
    2 即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候

    • 解决办法 - 加上readResolve方法
    //code 11
    package com.hollis;
    import java.io.Serializable;
    
    public class Singleton implements Serializable{
        private volatile static Singleton singleton;
        private Singleton (){}
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
        private Object readResolve() {
            return singleton;
        }
    }
    
    • 为什么?

    在jdk中ObjectInputStream的类中有readUnshared()方法,如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”(我也不明白),然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。

    4 单例懒汉式如何使用final

    //code 9
    class FinalWrapper<T> {
        public final T value;
    
        public FinalWrapper(T value) {
            this.value = value;
        }
    }
    
    public class FinalSingleton {
        private FinalWrapper<FinalSingleton> helperWrapper = null;
    
        public FinalSingleton getHelper() {
            FinalWrapper<FinalSingleton> wrapper = helperWrapper;
    
            if (wrapper == null) {
                synchronized (this) {
                    if (helperWrapper == null) {
                        helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
                    }
                    wrapper = helperWrapper;
                }
            }
            return wrapper.value;
        }
    }
    

    5 为什么饿汉式会线程安全,枚举类创建的单例会线程安全吗?

    当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)

    6 怎样破坏单例?为什么?


    1 反射可以破坏单例,这个是毋庸置疑的
    2 序列化也可以破坏单例

    • 序列化破坏的原因

    对象的序列化过程通过ObjectOutputStreamObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStreamreadObject 该方法通过反射的方式调用无参构造方法新建一个对象。

    6 为什么枚举可避免反序列化破坏单例

    在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject等方法。

    普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
    可以参考:ObjectInputputStreamreadObject的代码

    但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题

    相关文章

      网友评论

          本文标题:1 单例模式

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