IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    设计模式学习-单例模式

    Eric Sheng\'s Blog发表于 2016-06-29 22:04:00
    love 0
    • 场景
    • 基础版本(懒汉式,线程不安全)
    • 懒汉式,线程安全
    • 懒汉式,线程不安全
    • 双重检验锁(double checked locking)
    • 饿汉式 static final field法
    • 静态内部类 static nested class
    • 最优雅版本--枚举 Enum
    • 总结
    • 延伸阅读

    好久没写博文,最近学习一些设计模式,顺便记录一下。
    单实例Singleton设计模式可能是被讨论和使用的最广泛的一个设计模式了,这可能也是面试中问得最多的一个设计模式了。我们尝试从场景出发,来看看要怎么设计这个类。

    场景

    我们要得到一个类,整个系统中只能出现一个类的实例。这样的场景非常多,比如说一个国家,只有能有一个现任总统。仔细想想,要满足这一条件,我们觉得应该满足几个条件。

    1. 和大部分类不同,它的构造函数需要时私有的。否则的话,在任何地方大家都能够new这个实例,那么系统中始终不能保持一个实例的情况了。
    2. 既然没有构造函数,那么我如何实例化这个类呢?我们需要一个金泰的方式让其形成实例,给个方法吧--getInstance(),这个方法要判断现在系统中有没有这个实例,如果有,则返回;如果没有则调用私有的构造方法来实例化这个类,并存好,等下次来getInstance()调用时返回。
    3. 所以我们还需要一个私有的变量来存储这个实例。
    4. 我们使用时就可以用Singleton.getInstance()得到它了。

    根据这些条件我觉得我们可以得到朴素的教科书版本的代码:

    基础版本(懒汉式,线程不安全)

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

    看上去很美好,解决了我们上诉的条件,满足懒加载(只有用到的时候才会去创建这个实例)。然而该方法有一个致命的弱点,当系统中几个线程同时调用这个方法时,就很有可能会实例化出多个实例来。也就是说线程不安全。为了解决这个问题最简单的方法就是加Synchronize关键字。代码如下:

    懒汉式,线程安全

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

    好了,线程安全了,然而我们发现在每次调用getInstance()的方法是,我们都会上锁。但其实我们只需要在创建的时候上锁,而不创建的时候我们其实不需要上锁。如果在多线程的系统中有频繁的调用,那么这段代码的性能会比较低。那我们只在创建的时候加锁行不行?像这样:

    懒汉式,线程不安全

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

    看起来不错哦。应该没有问题了吧?!错!这还是有问题!为什么呢?前面已经说过,如果有多个线程同时通过(singleton== null)的条件检查(因为他们并行运行),虽然我们的synchronized方法会帮助我们同步所有的线程,让我们并行线程变成串行的一个一个去new,那不还是一样的吗?同样会出现很多实例。线程依然不安全,没有解决第一种方法的问题!!!!好了我知道了,这样,双重校验:

    双重检验锁(double checked locking)

    public class Singleton {
        private volatile static Singleton instance; //声明成 volatile
        private Singleton (){}
        public static Singleton getSingleton() {
            if (instance == null) {                         //Single Checked
                synchronized (Singleton.class) {
                    if (instance == null) {                 //Double Checked
                        instance = new Singleton();
                    }
                }
            }
            return instance ;
        }
    }
    

    但是这种方法要对volatile关键字有想到深刻的理解,并且对Java的内存模型深度理解。同时这种方法在jdk1.5之前是不能有bug的。如果你在面试中使用了这种方法,但是又不能很好的解释这方法的话。面试官不会喜欢你。-,-
    相信你不会喜欢这种复杂又隐含问题的方式,如果你仍有兴趣,请查看这里当然我们有更好的实现线程安全的单例模式的办法。

    饿汉式 static final field法

    这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

    public class Singleton{
        //类加载时就初始化
        private static final Singleton instance = new Singleton();
    
        private Singleton(){}
    
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    也就是说这种方法巧妙的避开了新建的过程,所以也不存在多线程调用时会产生的问题。大部分的情况下,这种方法能满足要求。但是吹毛求疵一下,这种方法在没用到它的时候它就已经实例化好了,它不是懒加载的形式。

    静态内部类 static nested class

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

    老版《Effective Java》中推荐的方法,上面这种方式,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

    最优雅版本--枚举 Enum

    用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

    public enum EasySingleton{
        INSTANCE;
    }
    

    居然用枚举!!看上去好牛逼,通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。

    默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。

    这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。

    总结

    小小的一个场景演化出了这么多方法。在一般的情况下饿汉式的方法用的比较多,在有懒加载要求时,静态内部类方法不错。

    延伸阅读

    • 如何正确地写出单例模式
    • 深入浅出单实例Singleton设计模式
    • 10-interview-questions-on-singleton
    • Double Checked Locking on Singleton Class in Java
    • Why Enum Singleton are better in Java


沪ICP备19023445号-2号
友情链接