孔乙己的疑问:单例模式有几种写法

引子

单例模式的文章能够说是百家争鸣,今天我也来讲道说道,你们共同提高。html

单例模式的做用和使用场景

单例模式(Singleton Pattern)

确保某一个类只有一个实例,并且能够自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 单例模式是一种对象建立型模式。数据库

使用场景

好比一个应用中应该只存在一个ImageLoader实例。设计模式

Android中的LayoutInflater类等。安全

EventBus中getDefault()方法获取实例。bash

保证对象惟一
  1. 为了不其余程序过多创建该类对象。先禁止其余程序创建该类对象
  2. 还为了让其余程序能够访问到该类对象,只好在本类中,自定义一个对象。
  3. 为了方便其余程序对自定义对象的访问,能够对外提供一些访问方式。

这三步怎么用代码体现呢?微信

  1. 将构造函数私有化。
  2. 在类中建立一个本类对象。
  3. 提供一个方法能够获取到该对象。

单例模式的十二种写法

1、饿汉式(静态变量)
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}
复制代码
2、饿汉式(静态常量)
public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

复制代码
3、饿汉式(静态代码块)
public class Singleton {
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}

复制代码

上面三种写法本质上实际上是同样的,也是各种文章在介绍饿汉式时经常使用的方式。但使用静态final的实例对象或者使用静态代码块依旧不能解决在反序列化、反射、克隆时从新生成实例对象的问题。多线程

序列化:一是能够将一个单例的实例对象写到磁盘,实现数据的持久化;二是实现对象数据的远程传输。 当单例对象有必要实现 Serializable 接口时,即便将其构造函数设为私有,在它反序列化时依然会经过特殊的途径再建立类的一个新的实例,至关于调用了该类的构造函数有效地得到了一个新实例!并发

反射:能够经过setAccessible(true)来绕过 private 限制,从而调用到类的私有构造函数建立对象。框架

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

优势:写法比较简单,在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。若是从始至终从未使用过这个实例,则会形成内存的浪费。


那么咱们就要考虑懒加载的问题了。

4、懒汉式(线程不安全)
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance== null) {
            instance = new Singleton();
        }
        return instance;
    }
}

复制代码

优势:懒加载,只有使用的时候才会加载。

缺点:可是只能在单线程下使用。若是在多线程下,instacnce对象仍是空,这时候两个线程同时访问getInstance()方法,由于对象仍是空,因此两个线程同时经过了判断,开始执行new的操做。因此在多线程环境下不可以使用这种方式。

5、懒汉式(线程安全,存在同步开销)
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

复制代码

优势:懒加载,只有使用的时候才会加载,获取单例方法加了同步锁,保正了线程安全。

缺点:效率过低了,每一个线程在想得到类的实例时候,执行getInstance()方法都要进行同步。

6、懒汉式(线程伪装安全,同步代码块)
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance  = new Singleton();
            }
        }
        return instance;
    }
}

复制代码

优势:改进了第五种效率低的问题。

缺点:但实际上这个写法还不能保证线程安全,和第四种写法相似,只要两个线程同时进入了 if (singleton == null) { 这句判断,照样会进行两次new操做


接下来就是听起来很牛逼的双重检测加锁的单例模式。

7、DCL「双重检测锁:Double Checked Lock」 单例(假)
public class Singleton {
    private static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance  = new Singleton();
                }
            }
        }
        return instance;
    }
}

复制代码

本例的亮点都在getInstance()方法上,能够看到在该方法中对instance进行了两次判空:第一层判断为了不没必要要的同步,第二层判断则是为了在null的状况下建立实例。对第六种单例的漏洞进行了弥补,可是仍是有丶小问题的,问题就在instance = new Singleton();语句上。

这语句在这里看起来是一句代码啊,但实际上它并非一个原子操做,这句代码最终会被编译成多条汇编指令,它大体作了3件事情:

  1. 给Singleton的实例分配内存
  2. 调用Singleton()的 构造函数,初始化成员字段
  3. 将instance对象指向分配的内存空间(此时instance就不是null了)

可是,因为Java编译器运行处理器乱序执行,以及jdk1.5以前Java内存模型中Cache、寄存器到主内存会写顺序的规定,上面的第二和第三的顺序是没法保证的。也就是说,执行顺序多是1-2-3也多是1-3-2.若是是后者,而且在3执行完毕、2未执行以前,被切换到线程B上,这时候instance由于已经在线程A内执行3了,instance已是非null,全部线程B直接取走instance,再使用时就会出错,这就是DCL失效问题,并且这种难以跟踪难以重现的问题极可能会隐藏好久。

优势:线程安全;延迟加载;效率较高。

缺点:JVM编译器的指令重排致使单例出现漏洞。

8、DCL「双重检测锁:Double Checked Lock」 单例(真,推荐使用)
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance  = new Singleton();
                }
            }
        }
        return instance;
    }
}

复制代码

在jdk1.5以后,官方已经注意到这种问题,调整了JVM、具体化了volatile关键字,所以,若是是1.5或以后的版本,只须要将instance的定义改为private static volatile Singleton instance = null;

就能够保证instance对象每次都是从主内存中读取,就可使用DCL的写法来完成单例模式。固然,volatile多少会影响到性能,但考虑到程序的正确性,牺牲这点性能仍是值得的。

优势:线程安全;延迟加载;效率较高。

缺点:因为volatile关键字会屏蔽Java虚拟机所作的一些代码优化,略微的性能下降,但除非你的代码在并发场景比较复杂或者低于JDK6版本下使用,不然,这种方式通常是可以知足需求的。

9、静态内部类(推荐使用)
public class Singleton {
    private Singleton() {
    }
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

复制代码

这种方式跟饿汉式方式采用的机制相似,但又有不一样。 二者都是采用了类装载的机制来保证初始化实例时只有一个线程。不一样的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的做用,而静态内部类方式在Singleton类被装载时并不会当即实例化,而是在须要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

因此在这里,利用 JVM的 classloder 的机制来保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁能够同步多个线程对同一个类的初始化

优势:避免了线程不安全,延迟加载,效率高。

缺点:依旧不能解决在反序列化、反射、克隆时从新生成实例对象的问题。

10、枚举
public enum Singleton {
    INSTANCE
}

复制代码

枚举类单例模式是《Effective Java》做者 Josh Bloch 极力推荐的单例方法

借助JDK 1.5中添加的枚举来实现单例模式。P.S. Enum是没有clone()方法的

  1. 枚举类类型是 final 的「不能够被继承」
  2. 构造方法是私有的「也只能私有,不容许被外部实例化,符合单例」
  3. 类变量是静态的
  4. 没有延时初始化,随着类的初始化就初始化了「从上面静态代码块中能够看出」
  5. 由 4 能够知道枚举也是线程安全的

优势:写法简单,不只能避免多线程同步问题,并且还能防止反序列化、反射,克隆从新建立新的对象。

缺点:JDK 1.5以后才能使用。

11、登记式单例--使用Map容器来管理单例模式
public class SingletonManger {
    private static Map<String, Object> objectMap = new HashMap<String, Object>();
    private SingletonManger() {
    }
    public static void registerService(String key, Object instance) {
        if (!objectMap.containsKey(key)) {
            objectMap.put(key, instance);
        }
    }
    public static Object getService(String key) {
        return objectMap.get(key);
    }
}

复制代码

查阅Android源码中的 LayoutInflater 对象就能发现使用了这种写法

优势:在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得咱们能够管理多种类型的单例,而且在使用时能够经过统一的接口进行获取操做, 下降了用户的使用成本,也对用户隐藏了具体实现,下降了耦合度。

缺点:不经常使用,有些麻烦

12、内部枚举类

在微信公众号看到有大佬说使用枚举配合内部类实现内部枚举类,能够达成线程安全,懒加载,责任单一原则,等等是如今最完美的写法。


四种需求的知足状况图

总结

若是你和我同样是Android开发,那么因为在客户端一般没有高并发的状况,选择哪一种实现方式并不会有太大的影响。但即使如此,出于效率考虑咱们也应该使用后面几种单例方法。

单例模式的优势

单例模式的优势其实已经在定义中提现了:能够减小系统内存开支,减小系统性能开销,避免对资源的多重占用、同时操做。

单例模式的缺点
  1. 违反了单一责任链原则,测试困难 单例类的职责太重,在必定程度上违背了“单一职责原则”。由于单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的建立和产品的自己的功能融合到一块儿。
  2. 扩展困难 因为单例模式中没有抽象层,所以单例类的扩展有很大的困难。修改功能必须修改源码。
  3. 共享资源有可能不一致。 如今不少面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,所以,若是实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将从新实例化,这将致使共享的单例对象状态的丢失。
注意在Application中存取数据

在Android 应用启动后、任意组件被建立前,系统会自动为应用建立一个 Application类(或其子类)的对象,且只建立一个。今后它就一直在那里,直到应用的进程被杀掉。

因此虽然 Application并无采用单例模式来实现,可是因为它的生命周期由框架来控制,和整个应用的保持一致,且确保了只有一个,因此能够被看做是一个单例。 可是若是你直接用它来存取数据,那你将获得无穷无尽的NullPointerException。

由于Application 不会永远驻留在内存里,随着进程被杀掉,Application 也被销毁了,再次使用时,它会被从新建立,它以前保存下来的全部状态都会被重置。

要预防这个问题,咱们不能用 Application 对象来传递数据,而是要:

  1. 经过传统的 intent 来显式传递数据(将 Parcelable 或 Serializable 对象放入Intent / Bundle。Parcelable 性能比 Serializable 快一个量级,可是代码实现要复杂一些)。

  2. 重写onSaveInstanceState()以及onRestoreInstanceState()方法,确保进程被杀掉时保存了必须的应用状态,从而在从新打开时能够正确恢复现场。

  3. 使用合适的方式将数据保存到数据库或硬盘。

  4. 老是作判空保护和处理。


参考文章

《Android 源码设计模式解析与实战》

www.cnblogs.com/zhaoyan001/…

www.jianshu.com/p/4f4f2fa7e…

www.jianshu.com/p/9b3587e8b…

相关文章
相关标签/搜索