孔乙己的疑问:单例模式有几种写法
引子
单例模式的文章可以说是百家争鸣,今天我也来说道说道,大家共同提升。
单例模式的作用和使用场景
单例模式(Singleton Pattern)
确保某一个类只有一个实例,而且可以自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 单例模式是一种对象创立型模式。
使用场景
比方一个应用中应该只存在一个ImageLoader实例。
Android中的LayoutInflater类等。
EventBus中getDefault()方法获取实例。
保证对象唯一
1. 为了避免其余程序过多建立该类对象。先禁止其余程序建立该类对象
2. 还为了让其余程序可以访问到该类对象,只好在本类中,自己设置一个对象。
3. 为了方便其余程序对自己设置对象的访问,可以对外提供少量访问方式。
这三步怎样用代码表现呢?
1. 将构造函数私有化。
2. 在类中创立一个本类对象。
3. 提供一个方法可以获取到该对象。
单例模式的十二种写法
一、饿汉式(静态变量)
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; }}
二、饿汉式(静态常量)
public class Singleton { private final static Singleton INSTANCE = new Singleton(); private Singleton() { } public static Singleton getInstance() { return INSTANCE; }}
三、饿汉式(静态代码块)
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的效果。假如从始至终从未使用过这个实例,则会造成内存的白费。
那么我们就要考虑懒加载的问题了。
四、懒汉式(线程不安全)
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance== null) { instance = new Singleton(); } return instance; }}
优点:懒加载,只有使用的时候才会加载。
缺点:但是只能在单线程下使用。假如在多线程下,instacnce对象还是空,这时候两个线程同时访问getInstance()方法,由于对象还是空,所以两个线程同时通过了判断,开始执行new的操作。所以在多线程环境下不可使用这种方式。
五、懒汉式(线程安全,存在同步开销)
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 instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }}
优点:改进了第五种效率低的问题。
缺点:但实际上这个写法还不能保证线程安全,和第四种写法相似,只需两个线程同时进入了 if (singleton == null) { 这句判断,照样会进行两次new操作
接下来就是听起来很牛逼的双重检测加锁的单例模式。
七、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编译器的指令重排导致单例出现漏洞。
八、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版本下使用,否则,这种方式一般是能够满足需求的。
九、静态内部类(推荐使用)
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 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化
优点:避免了线程不安全,推迟加载,效率高。
缺点:仍旧不能处理在反序列化、反射、克隆时重新生成实例对象的问题。
十、枚举
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之后才能使用。
十一、登记式单例–使用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获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作, 降低了客户的使用成本,也对客户隐藏了具体实现,降低了耦合度。
缺点:不常用,有些麻烦
十二、内部枚举类
在微信公众号看到有大佬说使用枚举配合内部类实现内部枚举类,可以达成线程安全,懒加载,责任单一准则,等等是现在最完美的写法。
四种需求的满足情况图
总结
假如你和我一样是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 源码设计模式解析与实战》
https://www.cnblogs.com/zhaoyan001/p/6365064.html
https://www.songma.com/p/4f4f2fa7e735
https://www.songma.com/p/9b3587e8b320
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 孔乙己的疑问:单例模式有几种写法