美文网首页shoucan gAndroid知识架构设计与重构
教你一步步写完美的单例模式

教你一步步写完美的单例模式

作者: Swy2w | 来源:发表于2017-05-03 16:32 被阅读1107次

    之前只会写固定的单例模式,没有仔细研究过。最佳在书上看到介绍一步步单例模式。不过是用cpp写的,与是自己用java一步步实现一遍。

    Step1 适应于单线程的Singleton

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

    我们将构造方法设为private避免了类在外部被实例化,这样在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。并且我们的方法和成员变量都是静态的。

    不足

    这种方法如果在单线程中使用是能够正常运行的,但是如果我们是在多线程情况下呢?想一想,如果有两个或以上的同学运行到判断instance是否为null的if语句的时候,这个时候如果instance没有创建,那么这些线程都会创建instance。这是就不满足我们的单例要求了。

    Step2 在多线程下的Singleton

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

    不细心的你可能会说这两行代码不是一样的吗。不对,我们对getInstance方法实现了同步锁。此时如果有多个线程想创建一个实例,因为在同一时刻只能由一个线程得到同步锁,当第一个线程加上锁时,后面的线程就需要等待。当第一个线程发现instance还没有创建的时候就会去创建。接着第一个线程释放同步锁,后面的线程加上同步锁,这时候实例已经由第一个线程创建了,所有第二个线程就不会重复的去创建了。

    不足

    虽然我们实现了多线程环境下的单例,但是你会发现我们每次在通过getInstance获取实例时,我们都视图去加上一个锁,但是加锁是一个耗时操作,我们应该尽量避免它。

    Step3 避免加锁带来的耗时

    我们只需要当我们的实例还没有创建的时候进行加锁防止多个线程同时创建实例,当实例已经创建的时候我们就应该避免加锁。

    public class Singleton {
        private Singleton() {}
        
        private volatile static Singleton INSTANCE=null;//使用volatile修饰禁止java重排序
        
        public static  Singleton getInstance(){
            if (INSTANCE==null) {
                synchronized (Singleton.class){
                    if (INSTANCE==null) {   //二次检测
                        INSTANCE=new Singleton();   
                    }
                }
            }
            return INSTANCE;
        }
    }
    

    我们在加锁前进行一次判断,这样只有当instance不存在的时候才需要加锁操作。这样我们的单例写的比较完美了,但是if语句的判断容易增加我们代码的错误率,精益求精的我们肯定不止步于此,再来想想更加优秀的解法。

    推荐解法一

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

    对了,上没的几种方法和解法一都是我们常说的懒汉模式。懒汉模式实现的是按需加载的单例模式。只有当我们需要的时候调用getInstance方法才会实例化这个单例。

    推荐解法二

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

    上面这种就是饿汉模式了,饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,需要的实例是已经存在的了。因为饿汉模式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

    相关文章

      网友评论

      • xjgd9900:枚举单例呢?
        xjgd9900: @Swy2w 那不补上才叫完美吗?
        Swy2w:@xjgd9900 java 1.5以后推荐使用枚举单例。但是我这里主要内容是分析一步步怎么来的就没写太多了
      • androidwing1992:volitale
        Swy2w:@androidwing1992 有人推荐用枚举好过用内部类。因为枚举可以反序列化。更加安全。
        Swy2w:@androidwing1992 对。step3应该加上:relieved:
      • 从入门到放弃:推荐解法二有缺陷,其实并不推荐。
        NIOAG37M:@Swy2w 每次都会获取锁,我想他的意思是这样。
        Swy2w:@从入门到放弃 具体缺陷,还望指点:blush: 感谢
      • alan峰:容器单例模式也是不错的
        Swy2w:@alan峰 我也是才涉及学习设计模式,多谢指教
      • 五香鱼cc:楼主step3也是有缺陷的,少考虑的编译重排问题。啪啪啪
        Swy2w:@五香鱼cc 对对对。谢谢指教:smile:
      • 波波杨某某:前排留名
      • C调路过:前两天跟公司大牛交流了下。如果单例的构造函数里需要初始化一些东西。那就懒汉。否则用饿汉也挺好
        Swy2w:受教了:stuck_out_tongue:

      本文标题:教你一步步写完美的单例模式

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