美文网首页Android设计模式Android
Android 架构5 设计模式之单例模式

Android 架构5 设计模式之单例模式

作者: 锐心凌志 | 来源:发表于2018-08-11 17:03 被阅读3次

    前言

    Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。
    单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

    1、单例模式UML类图

    2、单例模式的八种写法

    2.1饿汉模式

    顾名思义,饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如下:

    public class Singleton {   
        private static Singleton = new Singleton();
        private Singleton() {}
        public static getSignleton(){
            return singleton;
        }
    }
    
    

    这样做的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以就需要下面的懒汉法。

    2.2 饿汉模式变种
    public class Singleton {  
        private Singleton instance = null;  
         static {  
        instance = new Singleton();  
        }  
        private Singleton (){}
        public static Singleton getInstance() {  
        return this.instance;  
        }  
     }  
    
    

    表面上看起来差别挺大,其实上面那种差不多,都是在类初始化即实例化instance。.

    2.3不加锁懒汉模式(线程不安全)

    懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,就有很大可能导致重复创建对象。

    public class Singleton {
        private static Singleton singleton = null;
        private Singleton(){}
        public static Singleton getSingleton() {
            if(singleton == null) singleton = new Singleton();
            return singleton;
        }
    }
    
    
    2.4加锁懒汉模式(线程安全,但是耗时)

    这种写法考虑了线程安全,将对singleton的null判断以及new的部分使用synchronized进行加锁。同时,对singleton对象使用volatile关键字进行限制,保证其对所有线程的可见性,并且禁止对其进行指令重排序优化。如此即可从语义上保证这种单例模式写法是线程安全的。但是每次通过getInstance方法得到singleton实例的时候都有一个试图去获取同步锁的过程。而众所周知,加锁是很耗时的,对高并发操作很不友好。

    public class Singleton {
        private static volatile Singleton singleton = null;
    
        private Singleton(){}
    
        public static Singleton getSingleton(){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
            return singleton;
        }    
    }
    
    
    2.5双重校验锁( 兼顾线程安全和效率的写法)

    虽然上面这种写法是可以正确运行的,但是其效率低下,还是无法实际应用。因为每次调用getSingleton()方法,都必须在synchronized这里进行排队,而真正遇到需要new的情况是非常少的。所以,就诞生了第三种写法:

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

    这种写法被称为“双重检查锁”,顾名思义,就是在getSingleton()方法中,进行两次null检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。

    该种写法存在Java低版本中的问题
    内存模型

    那么,这种写法是不是绝对安全呢?前面说了,从语义角度来看,并没有什么问题。但是其实还是有坑。说这个坑之前我们要先来看看volatile这个关键字。其实这个关键字有两层语义。第一层语义相信大家都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。

    例如,考虑下面的事件序列:

    1. 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
    2. 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
    3. 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。
    Symantec JIT 编译 singletons[i].reference = new Singleton(); 这段代码时,如果不加volatile关键词,会生成如下字节码:
    
    0206106A   mov         eax,0F97E78h
    0206106F   call        01F6B210                  ; allocate space for
                                                     ; Singleton, return result in eax
    02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                    ; store the unconstructed object here.
    02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                     ; get the raw pointer
    02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
    0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
    02061086   mov         dword ptr [ecx+8],400h
    0206108D   mov         dword ptr [ecx+0Ch],0F84030h
    
    
    可以看到,在执行Singleton的构造函数之前,Singleton的新实例就被赋值给了singletons[i].reference,这在Java内存模型中是完全合法的。

    注意,前面反复提到“从语义上讲是没有问题的”,但是很不幸,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。

    2.6 静态内部类法(推荐)

    那么,有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的:

    public class Singleton {
        private static class Holder {
            private static Singleton singleton = new Singleton();
        }
    
        private Singleton(){}
    
        public static Singleton getSingleton(){
            return Holder.singleton;
        }
    }
    
    

    但是,上面提到的所有实现方式都有两个共同的缺点:

    • 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
    • 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
    2.7 枚举写法

    当然,还有一种更加优雅的方法来实现单例模式,那就是枚举写法:

        public  class Resource{
        }
    
        public enum SomeThing {
            INSTANCE;
            private Resource instance;
            SomeThing() {
                instance = new Resource();
            }
            public Resource getInstance() {
                return instance;
            }
        }
    
    

    调用

        Resource resource = SomeThing.INSTANCE.getInstance();
    
    

    使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。

    2.8 容器实现单例模式
    import java.util.HashMap;
    import java.util.Map;
    
    public class Singleton {
        private static Map<String, Object> objMap = new HashMap<String, Object>();
    
        private Singleton() {
        }
    
        public static void registerService(String key, Object instance) {
            if (!objMap.containsKey(key)) {
                objMap.put(key, instance);
            }
        }
    
        public static Object getService(String key) {
            return objMap.get(key);
        }
    }
    
    

    这种实现方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一接口进行获取操作,降低用户使用成本,也对用户隐藏了具体实现,降低耦合度。

    3 、单例模式在Android源码中应用

    第三方 ImageLoader(通过源码分析,得到单例模式中双重检测方案)
    LayoutInflater 单例模式通过容器进行管理
    LayoutInflater 源码分析 WindowManager、ActivityManager、PowerManager都是容器管理

    总结

    代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是jdk版本)下,自然有不同的最优解(或者说较优解)。
    比如枚举,虽然Effective Java中推荐使用,但是在Android平台上却是不被推荐的。在这篇Android Training中明确指出:

    Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

    再比如双重检查锁法,不能在jdk1.5之前使用,而在Android平台上使用就比较放心了(一般Android都是jdk1.6以上了,不仅修正了volatile的语义问题,还加入了不少锁优化,使得多线程同步的开销降低不少)。

    最后,不管采取何种方案,请时刻牢记单例的三大要点:

    • 线程安全
    • 延迟加载
    • 序列化与反序列化安全

    相关文章

      网友评论

        本文标题:Android 架构5 设计模式之单例模式

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