重学设计模式之单例模式

作者: 晨鸣code | 来源:发表于2017-08-31 14:41 被阅读78次

    单例模式应该是大家最为熟知的一种设计模式了,相信大家或多或少的都在自己的项目中使用过单例模式,例如封装一个Log工具类、一个数据库存取类或者用户登录管理类等。而我们使用单例模式主要有两个目的:

    1. 减少内存消耗
    2. 保证某些共享资源的唯一性

    单例模式的写法有好多种,如:饿汉式单例模式、懒汉式单例模式、IoDH单例模式、枚举式单例模式,下面就来一一实现下这些写法。

    饿汉式单例模式

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

    上面的例子中,类被加载时,静态变量instance就会被初始化,这时候单例类的唯一实例就被创建了。

    • 优点:类加载时就已经实例化,避免了线程问题
    • 缺点:由于类加载的时候就实例化了,没有达到懒加载的效果,可能会造成内存浪费

    饿汉式单例模式还要一种变种

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

    效果与前面的一样,不过把初始化方法放到静态代码块中,也是在类加载时调用。

    懒汉式单例模式

    低配版

    public class LazySingletonV1 {
    
        private static LazySingletonV1 instance ;
    
        private LazySingletonV1(){
        }
    
        public static LazySingletonV1 getInstance(){
            if(instance == null){
                instance = new LazySingletonV1();
            }
            return instance;
        }
    }
    

    上面的代码是最简单的懒汉式单例模式,实现懒加载,每次获取实例时会去判断是否已创建实例,如果一直没人用,则不用创建实例,节省内存空间。

    但实际上,上面这种写法应该是最不推荐的一种单例模式写法。因为它是线程不安全的,如果多个线程同时获取该实例,就会创建多个实例对象,不符合单例的需求。

    进阶版

    public class LazySingletonV2 {
    
        private static LazySingletonV2 instance ;
    
        private LazySingletonV2(){
        }
    
        public static synchronized LazySingletonV2 getInstance(){
            if(instance == null){
                instance = new LazySingletonV2();
            }
            return instance;
        }
    }
    

    可以看到在这种懒汉式单例模式中,我们在获取实例的方法上加了一个同步锁,这样保证了获取实例的方法在不同线程中是同步的,使得获取的实例是唯一的。

    但是这种写法有一个最大的问题就是,效率太低。每一个线程都要进行等待,而实际中,如果已经创建了,后面的想获取实例,直接返回就行。

    再进阶版

    public class LazySingletonV3 {
        private volatile static LazySingletonV3 instance;
    
        private LazySingletonV3() {
    
        }
    
        public static LazySingletonV3 getInstance() {
            if (instance == null) {
                synchronized (LazySingletonV3.class) {
                    instance = new LazySingletonV3();
                }
            }
            return instance;
        }
    }
    

    基于前一个版本的问题,有人想到了将同步锁置于instance为空判断之后,这样就实现了当实例已经创建,后面的获取时直接返回的问题。

    但是,这种写法又导致了线程不安全。如果A用户拿到了同步锁,正在创建实例,另一个B用户在实例还未创建时到了同步锁外等候,当A用户创建完实例,退出同步锁后,B用户马上就获取了同步锁并开始创建实例,这就导致了创建了多个实例。

    终极版

    //双重校验锁
    public class LazySingletonV4 {
        private  volatile static LazySingletonV4 instance;
    
        private LazySingletonV4(){
    
        }
    
        public static LazySingletonV4 getInstance(){
            //检查实例是否存在,不存在才进入同步块
            if(instance == null){
                //同步块,保证线程安全
                synchronized(LazySingletonV4.class){
                    //再次检查实例是否存在,不存在才创建实例
                    if(instance == null){
                        instance = new LazySingletonV4();
                    }
                }
            }
            return instance;
        }
    }
    

    双重校验算是懒汉式单例模式的终极版本了,先判断实例是否为空,为空获取同步锁,在同步锁内再判断一次实例是否为空,既保证了线程安全又提高了效率。

    双重校验加锁的实现一般会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

    IoDH单例模式

    什么是IoDH呢?

    IoDH是Initialization Demand Holder 的缩写,简单来说就是在单例类中增加一个静态内部类,在该内部类中创建单例类的实例。

    我们知道,在多线程开发时,为了解决并发问题,我们会使用synchronized来加互斥锁进行同步控制。但是在某些情况下,JVM已经隐含为您执行了同步,这些时候就不需要自己进行同步控制了。这些情况包括:

    1. 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
    2. 访问final字段时
    3. 在创建线程之前创建对象时
    4. 线程可以看见它将要处理的对象时

    例如饿汉式单例模式,就是在类加载时进行了初始化,也就是由静态初始化器初始化的。但是饿汉式单例模式不符合懒加载的要求,如果可以让类加载时不去初始化对象,不就解决问题了吗。这就是IoDH的方法,通过定义一个静态内部类,在这个静态内部类中创建单例类实例,当我们需要使用时才会去加载这个静态内部类,创建单例类实例。

    public class IoDHSingleton {
    
        private IoDHSingleton() {
    
        }
    
        private static class SingletonHolder {
            /**
             * 静态初始化器,由JVM来保证线程安全
             */
            private static IoDHSingleton instance = new IoDHSingleton();
        }
    
        public static IoDHSingleton getInstance() {
            return SingletonHolder.instance;
        }
    }
    

    当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建IoDHSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

    这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

    枚举式单例模式

    借助JDK1.5中添加的枚举来实现单例模式,应该是最好的实现单例模式的方式,代码也很简单。

    public enum EnumSingleton {
    
        instance;
    
    
        public void method(){
            //功能方法
        }
    }
    

    访问也很简单,通过EnumSingleton.instance即可调用枚举类中的方法了。这种单例模式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

    总结

    在实际项目中 饿汉式单例模式、懒汉式单例模式终极版、IoDH单例模式、枚举式单例模式 都是可以选择的实现方式,看个人喜好。不过枚举式单例模式应该是最简单且安全的实现方式,推荐使用。

    相关文章

      网友评论

        本文标题:重学设计模式之单例模式

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