设计模式之单例模式完全解析

来源:互联网 发布:北京耳机实体店 知乎 编辑:程序博客网 时间:2024/06/13 16:53

概述

在Android开发中我们对别的设计模式可能不了解或没见过,但是单例模式你是绝对见过或者写过的。但是在写的人里面有的真的了解单例模式而有的则是一知半解,那就造成了可能你写的单例带来的好处大于坏处,更或者坏处大于好处只是自己没有发现。
今天我就我目前的理解来谈一谈单例模式,帮助我们更好的来利用单例模式。(在java和Android的基础之上)

一.单例模式简介

1.1 概念

Singleton 模式可以是很简单的,一般的实现只需要一个类就可以完成,甚至都不需要UML图就能解释清楚。在这个唯一的类中,单例模式确保此类仅有一个实例,自行实例化并提供一个访问它的全局公有静态方法。

1.2使用场景

第一种:产生某对象会消耗过多的资源,为避免频繁地创建与销毁对象对资源的浪费。如: 对数据库的操作、访问 IO、线程池(threadpool)、网络请求等。
第二种:某种类型的对象应该有且只有一个。如果制造出多个这样的实例,可能导致:程序行为异常、资源使用过量、结果不一致等问题。如果多人能同时操作一个文件,又不进行版本管理,必然会有的修改被覆盖。所以: 一个系统只能有:一个窗口管理器或文件系统,计时工具或 ID(序号)生成器,缓存(cache),处理偏好设置和注册表(registry)的对象,日志对象。

1.3 优缺点

单例模式的优点:可以减少系统内存开支,减少系统性能开销,避免对资源的多重占用、同时操作。
单例模式的缺点:扩展很困难,容易引发内存泄露,测试困难,一定程度上违背了单一职责原则,进程被杀时可能有状态不一致问题。

二.单例模式的分类和实现

2.1 分类

按加载时机可以分为:饿汉方式和懒汉方式;按实现的方式,有:双重检查加锁,内部类方式和枚举方式等等。另外还有一种通过Map容器来管理单例的方式。

2.2 单例模式的实质和各种实现方式的选择原则

无论以哪种形式实现单例模式,本质都是使单例类的构造函数对其他类不可见,仅提供获取唯一一个实例的静态方法,必须保证这个获取实例的方法是线程安全的,并防止反序列化、反射、克隆(、多个类加载器、分布式系统)等多种情况下重新生成新的实例对象。
至于选择哪种实现方式则取决于项目自身情况,如:是否是复杂的高并发环境、JDK 是哪个版本的、对单例对象资源消耗的要求等。
它们有的效率很高,有的节省内存,有的实现得简单漂亮,还有的则存在严重缺陷,它们大部分使用的时候都有限制条件。下面我们来分析下各种写法的区别,辨别出哪些是不可行的,哪些是推荐的,最后为大家筛选出几个最值得我们适时应用到项目中的实现方式。

三.实现方式和适用场景

为了克服不是单例的情况出现,下面从以下几点来理解如何写好单例:

3.1线程安全

作为一个单例,我们首先要确保的就是实例的“唯一性”,有很多因素会导致“唯一性”失效,它们包括:多线程、序列化、反射、克隆等,更特殊一点的情况还有:分布式系统、多个类加载器等等。其中,多线程问题最为突出。为了提高应用的工作效率,现如今我们的工程中基本上都会用到多线程;目前使用单线程能轻松完成的任务,日复一日,随着业务逻辑的复杂化、用户数量的递增,也有可能要被升级为多线程处理。所以任何在多线程下不能保证单个实例的单例模式,我都认为应该立即被弃用。

3.1.1 “饿汉方式”实现的单例:

在只考虑一个类加载器的情况下,“饿汉方式”实现的单例(在系统运行起来装载类的时候就进行初始化实例的操作,由 JVM 虚拟机来保证一个类的初始化方法在多线程环境中被正确加锁和同步,所以)是线程安全的,换句话说就是天生丽质。
下面是实现的三种方式:
方式一:

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

方式二:

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

方式三:

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

这三种方式差别不大,都依赖 JVM 在类装载时就完成唯一对象的实例化,基于类加载的机制,它们天生就是线程安全的,所以都是可行的,第二种更易于理解也比较常见。

3.1.2 “懒汉方式”:

需要注意了,先来看一种最简单的“懒汉方式”的单例:

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

这种写法只能在单线程下使用。如果是多线程,可能发生一个线程通过并进入了 if (singleton == null) 判断语句块,但还未来得及创建新的实例时,另一个线程也通过了这个判断语句,两个线程最终都进行了创建,导致多个实例的产生。
所以在多线程环境下必须摒弃此方式。

同时,除了多并发的情况,实现单例模式时另一个重要的考量因素是效率。前述的“懒汉方式”的多线程问题可以通过加上 synchronized 修饰符解决,但考虑到性能,一定不要简单粗暴地将其添加在如下位置:

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

为什么解决了线程安全问题,为什么效率低下?
上述方式通过为 getInstence() 方法增加 synchronized 关键字,迫使每个线程在进入这个方法前,要先等候别的线程离开该方法,即不会有两个线程可以同时进入此方法执行 new Singleton(),从而保证了单例的有效。但它的致命缺陷是效率太低了,每个线程每次执行 getInstance() 方法获取类的实例时,都会进行同步。而事实上实例创建完成后,同步就变为不必要的开销了,这样做在高并发下必然会拖垮性能。所以此方法虽然可行但也不推荐。
解决的办法或者叫写法?如下:

public class Singleton  {    /*注意这里的volatile修饰语     * Java编译器允许处理器乱序执行,会有DCL失效的问题。     * (“双重检查锁定”(Double Check Lock(DCL))方式)     * JDK大于1.5的版本具体化了volatile关键字,     * 定义了加上它就可以保证执行的顺序,虽然会影响性能但是保证了单例有效*/    private static volatile Singleton singleton;    private Singleton (){}    public static Singleton getInstance(){        if (singleton == null) {//第一次check,避免不必要的同步(提高效率)            synchronized (Singleton.class){//同步                if (singleton == null){//第二次check,保证线程安全                    singleton = new Singleton();                }            }        }        return singleton;    }}

其实上面是直接将正确的方式写了出来,还有一种不健全的没有写,为了减小篇幅。
上面这种写法就是最终正确的写法,原因如下:
双重检查锁定(DCL)方式也是延迟加载的,它唯一的问题是,由于 Java 编译器允许处理器乱序执行,在 JDK 版本小于 1.5 时会有 DCL 失效的问题。当然,现在大家使用的 JDK 普遍都已超过 1.4,只要在定义单例时加上 1.5 及以上版本具体化了的 volatile 关键字,即可保证执行的顺序,从而使单例起效。所以 DCL 方式是推荐的一种方式。
(注意,关键字:volatile ,DCL ,懒汉)

同时在一些好的框架工具中如Android 中鼎鼎大名的 Universal Image Loader 和 EventBus 都是采用了这种方式的单例。
下面是节选的源码:首先是UIL的如下

private volatile static ImageLoader instance;/** Returns singleton class instance */public static ImageLoader getInstance() {    if (instance == null) {        synchronized (ImageLoader.class) {            if (instance == null) {                instance = new ImageLoader();            }        }    }    return instance;}protected ImageLoader() {}

下面是EventBus的:

static volatile EventBus defaultInstance;/** Convenience singleton for apps using a process-wide EventBus instance. */public static EventBus getDefault() {    if (defaultInstance == null) {        synchronized (EventBus.class) {            if (defaultInstance == null) {                defaultInstance = new EventBus();            }        }    }    return defaultInstance;}

在我之前的项目中的应用:
由于之前做过游戏平台的sdk所以就要提供给用户调用的工具类,这个工具类就直接写成了懒汉DCL形式的单例。同时由于项目中的资源类的东西相对较多,并且涉及到Activity的生命周期问题,所以在销毁的时候也可以说是在onDestory的时候加入了注销资源的一些操作。

3.1.3 “静态内部类”方式:

直接上代码先:

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

这种方式利用了 classloder 的机制来保证初始化 instance 时只会有一个。
需要注意的是:虽然它的名字中有“静态”两字,但它是属于“懒汉模式”的!!这种方式的 Singleton 类被装载时,只要 SingletonHolder 类还没有被主动使用,instance 就不会被初始化。只有在显式调用 getInstance() 方法时,才会装载 SingletonHolder 类,从而实例化对象。“静态内部类”方式基本上弥补了 DCL 方式在 JDK 版本低于 1.5 时高并发环境失效的缺陷。

《Java并发编程实践》中也指出 DCL 方式的“优化”是丑陋的,对静态内部类方式推崇备至。但是可能因为同大家创建单例时的思考习惯不太一致(根据单例模式的特点,一般首先想到的是通过 instance 判空来确保单例),此方式并不特别常见,然而它是所有懒加载的单例实现中适用范围最广、限制最小、最为推荐的一种。

3.1.4 使用枚举方式

简单的如下:

public enum  Singleton  {    SINGLETON;    private Singleton(){}}

几个好处:
1、 自由序列化;
2、 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);
3、 线程安全;
虽然很牛,但是在平时的开发中很少看到。这种方法有很多的好处上面的几点已经列出。具体的详情下面会提到。

3.2加载时机

除了高并发下的线程安全,对于单例模式另一个必须要考虑的问题是加载的时机,也就是要在延迟加载和急切加载间做出选择。

3.2.1 问题:

那么我们到底什么时候选择懒加载,什么时候选择饿加载呢?

3.2.2 分析:

首先,饿汉式的创建方式对使用的场景有限制。如果实例创建时依赖于某个非静态方法的结果,或者依赖于配置文件等,就不考虑使用饿汉模式了(静态变量也是同样的情况)。但是这些情况并不常见,我们主要考虑的还是两种方法对空间和时间利用率上的差别。饿汉式因为在类创建的同时就实例化了静态对象,其资源已经初始化完成,所以第一次调用时更快,优势在于速度和反应时间,但是不管此单例会不会被使用,在程序运行期间会一直占据着一定的内存;而懒汉式是延迟加载的,优点在于资源利用率高,但第一次调用时的初始化工作会导致性能延迟,以后每次获取实例时也都要先判断实例是否被初始化,造成些许效率损失。

3.2.3 结论:

所以这是一个空间和时间之间的选择题,如果一个类初始化需要耗费很多时间,或应用程序总是会使用到该单例,那建议使用饿汉模式;如果资源要占用较多内存,或一个类不一定会被用到,或资源敏感,则可以考虑懒汉模式。

3.2.4 注意:

有人戏称单例为“内存泄露”,即使一直没有人使用,它也占据着内存。所以再重申一遍,在使用单例模式前先考虑清楚是否必须,对于那些不是频繁创建和销毁,且创建和销毁也不会消耗太多资源的情况,不要因为首先想到的是单例模式就使用了它。

四.其他对单例模式的破坏和解决方法

4.1 序列化破坏

除了多线程,序列化也可能破坏单例模式一个实例的方式。序列化一是可以将一个单例的实例对象写到磁盘,实现数据的持久化;二是实现对象数据的远程传输。当单例对象有必要实现 Serializable 接口时,即使将其构造函数设为私有,在它反序列化时依然会通过特殊的途径再创建类的一个新的实例,相当于调用了该类的构造函数有效地获得了一个新实例!下述代码就展示了一般情况下行之有效的饿汉式单例,在反序列化情况下不再是单例。
下面是实验的代码(为了少写代码这里用了恶汉的,懒汉的一样哦):

单例类:public class Singleton implements Serializable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){}    public static Singleton getInstance(){        return INSTANCE;    }}测试类:public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        testSingleton();    }    private File mFile;    private void testSingleton() {        final Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        boolean isValid = instance1 == instance2;        Log.e("testSingleton", "Is Singleton Pattern valid: " + isValid);        new Thread(new Runnable() {            @Override            public void run() {                try {                    //Serialize                    createFile();                    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(mFile));                    oos.writeObject(instance1);                    oos.close();                    //Deserialize                    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(mFile));                    Singleton instance3 = (Singleton) ois.readObject();                    ois.close();                    boolean isValid1 = instance1 == instance3;                    Log.e("testSingleton", "Is Singleton Pattern valid after deserialize: " + isValid1);                } catch (Exception e) {                    e.printStackTrace();                }            }        }).start();    }    private void createFile() {        String basePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "GongSingleton";        String filePath = basePath +  File.separator + "singleton.txt";        try {            File file = new File(basePath);            if (!file.exists()){                file.mkdirs();            }            mFile = new File(filePath);            if (!mFile.exists()){                mFile.createNewFile();            }        } catch (IOException e) {            e.printStackTrace();        }    }}

日志:

03-23 17:21:21.154 13127-13127/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid: true03-23 17:21:21.174 13127-13912/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid after deserialize: false

从log中发现反序列化之后单例被破坏了。
要避免单例对象在反序列化时重新生成对象,则在 implements Serializable 的同时应该实现 readResolve() 方法,并在其中保证反序列化的时候获得原来的对象。
(注:readResolve() 是反序列化操作提供的一个很特别的钩子函数,它在从流中读取对象的 readObject(ObjectInputStream) 方法之后被调用,可以让开发人员控制对象的反序列化。我们在 readResolve() 方法中用原来的 instance 替换掉从流中读取到的新创建的 instance,就可以避免使用序列化方式破坏了单例。)
改为如下:

public class Singleton implements Serializable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){}    private Object readResolve(){        return INSTANCE;    }    public static Singleton getInstance(){        return INSTANCE;    }}
03-23 17:34:17.344 21567-21567/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid: true03-23 17:34:17.350 21567-21979/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid after deserialize: true

通过上面的方式就很好的解决了这个问题。
如果想要比较“优雅”地避免上述问题,最好的方式其实是使用枚举。
这种方式也是 Effective Java 作者 Josh Bloch 在 item 3 讨论中提倡的方式。
枚举不仅在创建实例的时候默认是线程安全的,而且在反序列化时可以自动防止重新创建新的对象。上面在提及枚举的方式实现的单例的时候已经列举了他的优点其中就包括了防止这个反序列化造成的单例失败。枚举类型是有“实例控制”的类,确保了不会同时有两个实例,即当且仅当 a=b 时 a.equals(b),用户也可以用 == 操作符来替代 equals(Object)方法来提高效率。使用枚举来实现单例还可以不用 getInstance() 方法(当然,如果你想要适应大家的习惯用法,加上 getInstance() 方法也是可以的),直接通过 Singleton.INSTANCE 来拿取实例。枚举类是在第一次访问时才被实例化,是懒加载的。它写法简单,并板上钉钉地保证了在任何情况(包括反序列化,以及后面会谈及的反射、克隆)下都是一个单例。不过由于枚举是 JDK 1.5 才加入的特性,所以同 DCL 方式一样,它对 JDK 的版本也有要求。因为此法在早期 JDK 版本不支持,且和一般单例写起来的思路不太一样,还没有被广泛使用,使用时也可能会比较生疏。所以在实际工作中,很少看见这种用法。

4.2 通过反射破坏

除了多线程、反序列化以外,反射也会对单例造成破坏。反射可以通过 setAccessible(true) 来绕过 private 限制,从而调用到类的私有构造函数创建对象。我们来看下面的代码:

单例类:public class Singleton implements Serializable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){}    public static Singleton getInstance(){        return INSTANCE;    }}测试类:public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        testSingleton();    }    private void testSingleton() {        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        boolean isValid = instance1 == instance2;        Log.e("testSingleton", "Is Singleton Pattern valid: " + isValid);        try {            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();            constructor.setAccessible(true);            Singleton instance3 = constructor.newInstance();            boolean isValid1 = instance1 == instance3;            Log.e("testSingleton", "Is Singleton Pattern valid after deserialize: " + isValid1);        } catch (Exception e) {            e.printStackTrace();        }    }}

日志:

03-23 17:51:15.693 404-404/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid: true03-23 17:51:15.693 404-404/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid after deserialize: false

通过上面的反射方式破坏了单例模式,想要避免这种情况的出现可以采用如下的方法:

public class Singleton implements Serializable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){        if (null != INSTANCE){//这里也可以通过计数或者使用flag的方式记录            throw new RuntimeException("Cannot construct Singleton more than once!");        }    }    public static Singleton getInstance(){        return INSTANCE;    }}

日志:

03-23 17:59:40.439 7058-7058/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid: true03-23 17:59:40.439 7058-7058/com.gong.servicetest W/System.err: java.lang.reflect.InvocationTargetException中间好多的Log信息没有用省略掉.....03-23 17:59:40.441 7058-7058/com.gong.servicetest W/System.err: Caused by: java.lang.RuntimeException: Cannot construct Singleton more than once!

另外,同反序列化相似,也可以使用枚举的方式来杜绝反射的破坏。当我们通过反射方式来创建枚举类型的实例时,会抛出“Exception in thread “main” java.lang.NoSuchMethodException: net.local.singleton.EnumSingleton.()”异常。所以虽然不常见,但是枚举确实可以作为实现单例的第一选择。

4.3 通过克隆破坏

clone() 是 Object 的方法,每一个对象都是 Object 的子类,都有clone() 方法。clone() 方法并不是调用构造函数来创建对象,而是直接拷贝内存区域。因此当我们的单例对象实现了 Cloneable 接口时,尽管其构造函数是私有的,仍可以通过克隆来创建一个新对象,单例模式也相应失效了。
如下所示:

单例类:public class Singleton implements Cloneable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){    }    public static Singleton getInstance(){        return INSTANCE;    }    //由于clone方法是protected方法,只有重写才能在MainActivity中调用,不然会invisable    @Override    protected Object clone() throws CloneNotSupportedException {        return super.clone();    }}测试类:public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        testSingleton();    }    private void testSingleton() {        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        boolean isValid = instance1 == instance2;        Log.e("testSingleton", "Is Singleton Pattern valid: " + isValid);        try {            Singleton instance3 = (Singleton) instance1.clone();            boolean isValid1 = instance1 == instance3;            Log.e("testSingleton", "Is Singleton Pattern valid after deserialize: " + isValid1);        } catch (Exception e) {            e.printStackTrace();        }    }}
日志如下:03-24 09:59:16.964 21023-21023/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid: true03-24 09:59:16.964 21023-21023/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid after deserialize: false

通过上面的log可以看出单例被破坏了。
所以单例模式的类是不可以实现 Cloneable 接口的,这与 Singleton 模式的初衷相违背。那要如何阻止使用 clone() 方法创建单例实例的另一个实例?可以 override 它的 clone() 方法,使其抛出异常。(也许你想问既然知道了某个类是单例且单例不应该实现 Cloneable 接口,那不实现该接口不就可以了吗?事实上尽管很少见,但有时候单例类可以继承自其它类,如果其父类实现了 clone() 方法的话,就必须在我们的单例类中复写 clone() 方法来阻止对单例的破坏。)
可以改为如下的写法:

public class Singleton implements Cloneable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){    }    public static Singleton getInstance(){        return INSTANCE;    }    @Override    protected Object clone() throws CloneNotSupportedException{        throw new CloneNotSupportedException();    }}
更改后log:03-24 10:26:24.775 21023-21023/com.gong.servicetest E/testSingleton: Is Singleton Pattern valid: true03-24 10:26:24.775 21023-21023/com.gong.servicetest W/System.err: java.lang.CloneNotSupportedException

P.S. Enum 是没有 clone() 方法的。

4.4总结

想要在很多复杂的情况下保证我们的单例模式不被破坏就要解决如上的几个问题:
a.线程安全问题;
b.反序列化问题;
c.反射问题;
d.克隆问题
(ps:上面也都一一克服了,并且介绍了一个较少使用但是很有效的避免了这些问题的方式来实现单例模式,那就是枚举;)
下面是利用恶汉式写的完整的方案(懒汉的相信你也会写了):

public class Singleton implements Serializable, Cloneable{    private static final Singleton INSTANCE = new Singleton();    private Singleton(){        if (null != INSTANCE){            throw new RuntimeException("Cannot construct Singleton more than once!");        }    }    private Object readResolve(){        return INSTANCE;    }    public static Singleton getInstance(){        return INSTANCE;    }    @Override    protected Object clone() throws CloneNotSupportedException{        throw new CloneNotSupportedException();    }}

当然在实际开发的时候我们可以避免一些情况比如克隆的问题,同时实际开发中我们也知道我们的使用场景从而我们可以不用写这么复杂。

五.登记式单例——使用 Map 容器来管理单例模式

在我们的程序中,随着迭代版本的增加,代码也越来越复杂,往往会使用到多个处理不同业务的单例,这时我们就可以采用 Map 容器来统一管理这些单例,使用时通过统一的接口来获取某个单例。在程序的初始,我们将一组单例类型注入到一个统一的管理类中来维护,即将这些实例存放在一个 Map 登记薄中,在使用时则根据 key 来获取对象对应类型的单例对象。对于已经登记过的实例,从 Map 直接返回实例;对于没有登记的,则先登记再返回。从而在对用户隐藏具体实现、降低代码耦合度的同时,也降低了用户的使用成本。
简易版代码实现如下:

public class SingletonManager {    private static Map<String, Object> map = new HashMap<String, Object>();    private SingletonManager(){}    public static void registerService(String key, Object instance){        if (!map.containsKey(key)){            map.put(key, instance);        }    }    public static Object getService(String key){        return map.get(key);    }}

Android 的系统核心服务就是以如上形式存在的,以达到减少资源消耗的目的。其中最为大家所熟知的服务有 LayoutInflater Service,它就是在虚拟机第一次加载 ContextImpl 类时,以单例形式注册到系统中的一个服务,其它系统级的服务还有:WindowsManagerService、ActivityManagerService 等。JVM 第一次加载调用 ContextImpl 的 registerService() 方法,将这些服务以键值对的形式(以 service name 为键,值则是对应的 ServiceFetcher)存储在一个 HashMap 中,要使用时通过 key 拿到所需的 ServiceFetcher 后,再通过 ServiceFetcher 的 getService() 方法来获取具体的服务对象。在第一次使用服务时,ServiceFetcher 调用 createService() 方法创建服务对象,并缓存到一个列表中,下次再取时就可以直接从缓存中获取,无需重复创建对象,从而实现单例的效果。

六.单例模式在使用过程中遇到的复杂问题

6.1 多个类加载器

是的,其实前文有提到过,上述的所有讨论都是基于一个类加载器(class loader)的情况。由于每个类加载器有各自的命名空间,static 关键词的作用范围也不是整个 JVM,而只到类加载器,也就是说不同的类加载器可以加载同一个类。所以当一个工程下面存在不止一个类加载器时,整个程序中同一个类就可能被加载多次,如果这是个单例类就会产生多个单例并存失效的现象。因此当程序有多个类加载器又需要实现单例模式,就须自行指定类加载器,并要指定同一个类加载器。基于同样的原因,分布式系统和集群系统也都可能出现单例失效的情况,这就需要利用数据库或者第三方工具等方式来解决失效的问题了。
当然上面的情况大多出现在后台用的java编程中。可以参看:https://my.oschina.net/pingpangkuangmo/blog/376328

6.2单例的构造函数是私有的,那还能不能继承单例?

单例是不适合被继承的,要继承单例就要将构造函数改成公开的或受保护的(仅考虑 Java 中的情况),这就会导致:
1)别的类也可以实例化它了,无法确保实例“独一无二”,这显然有违单例的设计理念。
2) 因为单例的实例是使用的静态变量,所有的派生类事实上是共享同一个实例变量的,这种情况下要想让子类们维护正确的状态,顺利工作,基类就不得不实现注册表(Registry)功能了。要实现单例模式的代码非常简洁,任意现有的类,添加十数行代码后,就可以改造为单例模式。也许继承并不是一个好主意。同时,也应该审视一下单例模式是否在此处被滥用了,在需要继承和扩展的情况下,一开始就不要使用单例模式,这会为你省下很多时间。
权衡:总之,决定一下对你的需求来说,到底是单例更重要还是可继承更重要。

6.3 单例有没有违反“单一责任原则”?

单例确实承担了两个责任,它不仅仅负责管理自己的实例并提供全局访问,还要处理应用程序的某个业务逻辑。但是由类来管理自己的实例的方式可以让整体设计更简单易懂,单例类自己负责实例的创建也已经是很多程序员耳熟能详的做法了,何况单例模式的创建只需要屈指可数的几行代码,在结构不复杂的情况下,单独将其移到其它类中并不一定经济。当然在代码繁复的情况下优化你的设计,让单例类专注于自己的业务责任,将它的实例化以及对对象个数的控制封装在一个工厂类或生成器中,也是较好的解决方案。

6.4 是否可以把一个类的所有方法和变量都定义为静态的,把此类直接当作单例来使用?

Java 里的 java.lang.System 类以及 java.lang.Math 类都是这么做的,它们的全部方法都用 static 关键词修饰,包装起来提供类级访问。可以看到,Math 类把 Java 基本类型值运算的相关方法组织了起来,当我们调用 Math 类的某个类方法时,所要做的都只是数据操作,并不涉及到对象的状态,对这样的工具类来说实例化没有任何意义。所以如果一个类是自给自足的,初始化简洁,也不需要维护任何状态,仅仅是需要将一些工具方法集中在一起,并提供给全局使用,那么确实可以使用静态类和静态方法来达到单例的效果。但如果单例需要访问资源并对象状态是关注点之一时,则应该使用普通的单例模式。静态方法会比一般的单例更快,因为静态的绑定是在编译期就进行的。但是也要注意到,静态初始化的控制权完全握在 Java 手上,当涉及到很多类时,这么做可能会引起一些微妙而不易察觉的,和初始化次序有关的bug。除非绝对必要,确保一个对象只有一个实例,会比类只有一个单例更保险。

6.5 考虑技术实现时,如何从单例模式和全局变量中作出选择?

全局变量虽然使用起来比较简单,但相对于单例有如下缺点:
1) 全局变量只是提供了对象的全局的静态引用,但并不能确保只有一个实例;
2) 全局变量是急切实例化的,在程序一开始就创建好对象,对非常耗费资源的对象,或是程序执行过程中一直没有用到的对象,都会形成浪费;
3) 静态初始化时可能信息不完全,无法实例化一个对象。即可能需要使用到程序中稍后才计算出来的值才能创建单例;
4) 使用全局变量容易造成命名空间(namespace)污染。(命名空间(namespace)污染的解释http://blog.csdn.net/ipmux/article/details/17376297)

6.6据说垃圾收集器会将没有引用的单例清除?

比较早的 Java 版本(JVM ≤ 1.2)的垃圾收集器确实有 bug,会把没有全局引用的单例当作垃圾清除。假设一个单例被创建并使用以后,它实例里的一些变量发生了变化。此时引用它的类被销毁了,除了它本身以外,再没有类引用它,那么一小会儿后,它会就被 Java 的垃圾收集器给清除了。这样再次调用此单例类的 getInstance() 时会重新生成一个单例,使用时会发现之前更新过的实例的变量值都回到了最原始的设置(如网络连接被重新设置等),一切都混乱了。这个 bug 在 1.2 以后的版本已经被修复,但是如果还在使用 Java 1.3 之前的版本,必须建立单例注册表,增加全局引用来避免垃圾收集器将单例回收。所以我们根本不用担心哦。

6.7 可以用单例对象 Application 来解决组件见传递数据的问题吗?

当然不行!!!!
下面是解释:
在 Android 应用启动后、任意组件被创建前,系统会自动为应用创建一个 Application 类(或其子类)的对象,且只创建一个。从此它就一直在那里,直到应用的进程被杀掉。所以虽然 Application 并没有采用单例模式来实现,但是由于它的生命周期由框架来控制,和整个应用的保持一致,且确保了只有一个,所以可以被看作是一个单例。一个 Android 应用总有一些信息,譬如说一次耗时计算的结果,需要被用在多个地方。如果将需要传递的对象塞到 intent 里或者存储到数据库里来进行传递,存取都要分别写代码来实现,还是有点麻烦的。既然 Application(或继承它的子类)对于 App 中的所有 activity 和 service 都可见,而且随着 App 启动,它自始至终都在那里,就不禁让我们想到,何不利用 Application 来持有内部变量,从而实现在各组件间传递、分享数据呢?这看上去方便又优雅,但却是完全错误的一种做法!!如果你使用了如上做法,那你的应用最终要么会因为取不到数据发生 NullPointerException 而崩溃,要么就是取到了错误的数据。

我们来看一个具体的例子:
1) 在我们的 App 启动后的第一个 Activity A 中,会要求用户输入需要显示的字符串,假设为 “Hello, Singlton!”,然后我们把它作为全局变量 showString 保存在 Application 中;
2) 然后从 Activity A 中 startActivity() 跳转到 Activity B,我们从 Application 对象中将 showString 取出来并显示到屏幕上。目前看起来,一切都很正常。
3) 但是如果我们按了 Home 键将 App 退到后台,那么在等了较长的时间后,系统可能会因为内存不够而回收了我们的应用。(也可以直接手动杀进程。)
4) 此时再打开我们的 App,系统会重新创建一个 Application 对象,并恢复到刚刚离开时的页面,即跳转到 Activity B。
5) 当 Activity B 再次运行到向 Application 对象拿取 showString 并显示时,就会发现现在显示的不再是“Hello, Singlton!”了,而是空字符串。这是因为在我们新建的 Application 对象中,showString并没有被赋值,所以为 null。如果我们在显示前先将字符串全部变为大写,showString.toUpperCase(),我们的程序甚至会因此而 crash!!究其本质,Application 不会永远驻留在内存里,随着进程被杀掉,Application 也被销毁了,再次使用时,它会被重新创建,它之前保存下来的所有状态都会被重置。

要预防这个问题,我们不能用 Application 对象来传递数据,而是要:
1) 通过传统的 intent 来显式传递数据(将 Parcelable 或 Serializable 对象放入Intent / Bundle。Parcelable 性能比 Serializable 快一个量级,但是代码实现要复杂一些)。
2) 重写 onSaveInstanceState() 以及 onRestoreInstanceState() 方法,确保进程被杀掉时保存了必须的应用状态,从而在重新打开时可以正确恢复现场。
3) 使用合适的方式将数据保存到数据库或硬盘。
4) 总是做判空保护和处理。
上述这个问题除了 Application 类存在,App 中的任何一个单例或者公共的静态变量都存在,这就要求我们写出健壮的代码来好好来维护它们的状态,也要在考虑是否使用单例时慎之又慎。

6.8 在 Android 中使用单例还有哪些需要注意的地方

单例在 Android 中的生命周期等于应用的生命周期,所以要特别小心它持有的对象是否会造成内存泄露。如果将 Activity 等 Context 传递给单例又没有释放,就会发生内存泄露,所以最好仅传递给单例 Application Context。

七.总结

下面的几种是比较好的推荐的,选择使用哪种单例模式依照你的使用场景。
恶汉式单例;
DCL单例;
静态内部类单例;
枚举型单例
具体的代码上面已经给出了。
参考:
https://my.oschina.net/pingpangkuangmo/blog/376328
http://blog.csdn.net/ipmux/article/details/17376297
http://blog.csdn.net/tencent_bugly/article/details/61915271

0 0
原创粉丝点击