• 设计之禅——单例模式详解


    一、前言

    有时候我们只需要一个类只有一个对象,如,线程池、缓存、windows的任务管理器、注册表等,因此就有了单例模式,确保了一个类只存在一个实例。单例模式的实现非常简单,但是其中的细节也需要注意。下面我们就来看看他的各种实现。

    二、实现

    单例模式的实现方式有很多,根据是否立即创建对象分为“懒汉”和“饿汉”两大类别,即是否在类加载时立即创建对象,如果该对象频繁被使用,可以使用“饿汉式”提高效率;反之则可以使用“懒汉式”来避免内存的浪费。而“懒汉式”的创建在多线程环境下则有许多方式来保证线程安全。

    1. 懒汉式-线程不安全

    public class Singleton {
    	
        public static Singleton instance;
        
     	// 私有化构造方法,保证外部无法创建对象
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
    
            return instance;
        }
    
    }
    

    这种只能保证在单线下获取到单例对象,并且在需要的时候才会创建对象,故此称为“懒汉式”。但是因为new Singleton()该操作并不是原子操作,当线程1执行到此时,可能还并未创建实例,那么线程2在判断instance==null时就会为真,从而产生多个实例。

    2. 懒汉式-线程安全

    public class Singleton {
    
        private static Singleton instance;
    
    	// 私有化构造方法,保证外部无法创建对象
        private Singleton() {}
    
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
    
            return instance;
        }
    
    }
    

    这种方式保证了线程安全,但是效率非常低,因此一般不推荐使用。

    3. 饿汉式

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

    与懒汉式的区别是类加载的时候立即创建了对象实例,保证了对象始终只会有一个,但是如果该对象一直不被使用,就会浪费内存资源。

    4. 静态内部类

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

    使用静态内部类来实现单例其实也是懒汉式的优化实现,利用类初始化时线程安全这一特点来创建单例对象,同时因为是在静态内部类中,有且仅当getInstance()方法被调用时才会被初始化,所以也避免了内存的浪费。

    5. 双重校验锁

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

    该方式是“饿汉式”的变种,保留了“饿汉式”的特点的同时保证了线程的安全。但是,需要注意的是volatile关键字是必须的,在网上很多文章上看到都没带这个关键字,如果不加可能会导致程序的崩溃。因此该方法只能在JDK1.5后使用。
    volatile是保证线程之间的可见性。倘若没有该关键字,假设线程1和线程2先后调用getInstance()方法,当线程1进入方法时判断instance=null,因此去执行new Singleton()创建实例,上文提到该操作并非原子操作,会被编译为三条指令:

    1. 分配对象的内存空间;
    2. 初始化对象;
    3. 将对象指向内存地址;

    而jvm会为了执行效率而进行指令重排,重排后的指令顺序为:1->3->2,当指令执行完第3条指令,此时线程2进入方法进行第一次判断时,就会得到一个并不完整的对象实例(因为对象还未初始化,只是分配了内存空间),接着线程1执行完第2条指令,又会返回这个实例的完全态,但并不会立即刷新主内存,所以线程2并不能访问到,程序就会出现错误导致崩溃。而volatile就是为了处理这个问题,他能保证当某个线程改变对象实例后,立即刷新主内存,让其他线程能够同样获取到相同的实例对象,就不会出现不一致的问题了。

    6. 枚举

    public enum Singleton {
        INSTANCE;
    }
    

    用枚举的方式创建单例非常简单明了,它本身能保证线程的安全,还能防止反序列化(readObject())导致对象不一致的问题,唯一的缺点则是同饿汉式一样会立即创建对象实例(反编译后可以看到),如果不考虑这点枚举应是单例实现的最佳方式,也是《Effective Java》作者推荐的方式。

    三、总结

    单例模式是比较常用的模式之一,本文总结了6种实现方式,可以感受到看似简单的代码背后涉及到的细节非常多,因此也是非常考验我们的基本功。在本文中并没有考虑反射入侵的情况,有兴趣的读者们可自行研究。

  • 相关阅读:
    51单片机入门(三)
    51单片机入门笔记(二)
    51单片机入门笔记
    团队项目-需求分析报告
    团队项目-选题报告
    第一次结对编程作业
    第一次个人编程作业
    第一次博客作业
    tomcat的安装和配置
    循环
  • 原文地址:https://www.cnblogs.com/yewy/p/13111853.html
Copyright © 2020-2023  润新知