设计模式之单例模式详解
单例模式写法大全,也许有你不知道的写法
导航
- 引言
- 什么是单例?
- 单例模式作用
- 单例模式的实现方法
引言
单例模式想必是大家接触的比较多的一种模式了,就算没用过但是肯定听过他的鼎鼎大名了。在我初入编程界时听到最多的就是单例模式,工厂模式,观察者模式了。特别是观察者模式在Android开发中几乎是随处可见,不过今天我们先来学习一个看似简单很多的单例模式。
什么是单例模式?
单例模式确保某一个类只有一个实例。
单例模式有什么用?
为什么要确保一个类只有一个实例?有什么时候才需要用到单例模式呢?听起来一个类只有一个实例好像没什么用呢!
那我们来举个例子。比如我们的APP中有一个类用来保存运行时全局的一些状态信息,如果这个类实现不是单例的,那么App里面的组件能够随意的生成多个类用来保存自己的状态,等于大家各玩各的,那这个全局的状态信息就成了笑话了。而如果把这个类实现成单例的,那么不管App的哪个组件获取到的都是同一个对象(比如Application类,除了多进程的情况下)。
怎么实现单例模式?
单例模式的定义和功能都是比较简单清楚的东西,那么到底怎么实现这个模式呢?
1.饿汉式
可能有的小伙伴们会想到利用Java的静态域初始化机制来实现
public class SimpleSingleton {
private static SimpleSingleton instance=new SimpleSingleton();
/**
* 构造方法私有化,几乎所有的单例模式实现都会将构造方法私有化
*/
private SimpleSingleton() {
}
public static SimpleSingleton getInstance(){
return instance;
}
}
这种写法很简单,先把构造函数设为private的(几乎大部分的单例写法都会这么做)。然后在类中设置一个静态的字段,并调用构造函数。这样jvm在加载这个类的时候就会自动初始化这个类,接着每次需要使用的时候都调用getInstance方法获得类实例。而java的语法规则保证new SimpleSingleton()自会调用一次。
使用这种方式实现的单例是线程安全的(这一点是由jvm保证的),并且在类加载的时候就已经生成了一个实例,当调用的时候get获取这个实例是就非常快。
凡事都有优缺点,饿汉式单例也不例外。他的一个很明显的缺点就是在性能上。jvm会在加载类的时候直接初始化实例,而如果这个类的实例在应用中使用频率并不高,有的时候整个App从被用户打开到结束都不会使用一次这个类实例,那么这个初始化的操作就是完全浪费了。
为了解决这个问题,我们想了一种新的实现方式。
2.懒汉式
public class ServiceNotThreadSafe {
public static ServiceNotThreadSafe INSTANCE = null;
/**
* 必备操作
*/
private ServiceNotThreadSafe() {
}
/**
* @return instance
*/
public static ServiceNotThreadSafe getInstance() {
if (INSTANCE == null) {
INSTANCE = new ServiceNotThreadSafe();
}
return INSTANCE;
}
}
懒汉式的写法有一个懒加载的效果,只有当第一次调用getInstance方法时才会去实例化一个对象。可能细心的小伙伴已经注意到这个很直白的类名了NotThreadSafe。没错这种写法在单线程中没有任何问题,但是在并发程序中就无法保证 类实例只有一个的情况了。
为了解决上面的问题,我们相出了一个新的写法
3.懒汉式 —— 单锁定法
public class ServiceThreadSafe {
public static ServiceThreadSafe instance;
/**
* 还是常规操作的私有构造函数
*/
private ServiceThreadSafe() {
}
public static synchronized ServiceThreadSafe getInstance(){
if (instance==null){
instance=new ServiceThreadSafe();
}
return instance;
}
}
这种写法没什么好说的,只是在getInstance方法上加了一个内置同步锁,从而保证了线程安全。但是也因此引入了一个新的问题 —— 同步锁范围太大,影响并发性能(在getInstance方法并不是频繁调用下问题不大)。
为了解决这个问题,聪明的程序员们想到了另一种写法。
4.双重锁定法
public class ServiceDoubleCheck {
/**
* 注意这里加了 volatile 修饰符,用来保证内存可见性(限制指令重排序)。
* 具体各位小伙伴可以Google一下。
* 如果你没加这个修饰符的话,那么具体结果只能看编译器,jvm和cpu的心情了O(∩_∩)O~~
*/
public static volatile ServiceDoubleCheck instance = null;
/**
* 大家都懂得操作
*/
private ServiceDoubleCheck() {
}
/**
* 理论上只要第一次的时候才会完全走完整个方法,之后进入这个方法时instance==null都不成立
* 而不用在进入内部的同步代码块,带来新能上的优势
* @return
*/
public static ServiceDoubleCheck getInstance() {
if (instance == null) {
synchronized (ServiceDoubleCheck.class) {
if (instance == null) {
instance = new ServiceDoubleCheck();
}
}
}
return instance;
}
}
优点:
- 资源利用率高,懒加载的形式,不使用就不会实例化
- 线程安全
缺点: - 写法略微繁琐
- 第一次加载时速度不快
5.懒汉式静态内部类写法
public class ServiceInner {
/**
* 实现一个静态内部类
*/
private static class Instance{
private static ServiceInner instance=new ServiceInner();
}
public static ServiceInner getInstance(){
return Instance.instance;
}
private ServiceInner() {
}
}
优点:
- 线程安全
- 懒加载形式,资源利用率高
缺点: - 第一次加载速度不快
6.枚举实现 —— 一个《effective java》 作者都推荐的方法
public class Resources {
public enum ResourcesInstance {
INSTANCE;
private Resources instance;
ResourcesInstance() {
this.instance = new Resources();
}
public Resources getInstance() {
return instance;
}
}
}
之前介绍过的哪些单例实现都有一个问题,就是不能保证序列化生成另一个实例。比如先序列化写入到文件,然后再从文件读取反序列化回来,这样子我们就会得到两个实例,这就违背了单例的原则。而用枚举实现能解决这个问题。
总结
基本上单例的实现方法都介绍完了,一般实际应用中如果对象的实例化并不是很耗费资源的话使用最简单的饿汉法就行了。如果需要对象懒加载则可以选用双重锁定法(如果不需要考虑线程安全的话可以使用简单的懒汉式)。而要在序列化的过程中保证单例的话就要使用枚举的方法来实现了。