前段时间公司一些同事在讨论单例模式(我是最渣的一个,都插不上嘴 T__T ),这个模式使用的频率很高,也可能是很多人最熟悉的设计模式,当然单例模式也算是最简单的设计模式之一吧,简单归简单,但是在实际使用的时候也会有一些坑。
PS:对技术感兴趣的同鞋加群544645972一起交流。设计模式总目录
特点
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的使用很广泛,比如:线程池(threadpool)、缓存(cache)、对话框、处理偏好设置、和注册表(registry)的对象、日志对象,充当打印机、显卡等设备的驱动程序的对象等,这些类的对象只能有一个实例,如果制造出多个实例,就会导致很多问题的产生,程序的行为异常,资源使用过量,或者不一致的结果等,所以单例模式最主要的特点:- 构造函数不对外开放,一般为private;
- 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且只有一个,尤其是在多线程的环境下;
- 确保单例类对象在反序列化时不会重新构建对象。
- 主要优点单例模式的主要优点如下:
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例对象共享过多有损性能的问题。
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
- 单例对象如果持有Context,那么很容易引发内存泄漏,此时需要注意传递给单例对象的Context最好是 Application Context。
UML类图
类图很简单,Singleton 类有一个 static 的 instance对象,类型为 Singleton ,构造函数为 private,提供一个 getInstance() 的静态函数,返回刚才的 instance 对象,在该函数中进行初始化操作。示例与源码
单例模式的写法很多,总结一下:
lazy initialization, thread-unsafety(懒汉法,线程不安全)
延迟初始化,一般很多人称为懒汉法,写法一目了然,在需要使用的时候去调用getInstance()函数去获取Singleton的唯一静态对象,如果为空,就会去做一个额外的初始化操作。
public class Singleton { private static Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if(instance == null) instance = new Singleton(); return instance; }}复制代码
需要注意的是这种写法在多线程操作中是不安全的,后果是可能会产生多个Singleton对象,比如两个线程同时执行getInstance()函数时,然后同时执行到 new 操作时,最后很有可能会创建两个不同的对象。
lazy initialization, thread-safety, double-checked(懒汉法,线程安全)
需要做到线程安全,就需要确保任意时刻只能有且仅有一个线程能够执行new Singleton对象的操作,所以可以在getInstance()函数上加上 synchronized 关键字,类似于:
public static synchronized Singleton getInstance() { if(singleton == null) instance = new Singleton(); return instance; }复制代码
但是套用《Head First》上的一句话,对于绝大部分不需要同步的情况来说,synchronized 会让函数执行效率糟糕一百倍以上(Since synchronizing a method could in some extreme cases decrease performance by a factor of 100 or higher),所以就有了double-checked(双重检测)的方法:
public class Singleton { private volatile static Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(); } } } return instance; }}复制代码
我们假设两个线程A,B同时执行到了getInstance()这个方法,第一个if判断,两个线程同时为true,进入if语句,里面有个 synchronized 同步,所以之后有且仅有一个线程A会执行到 synchronized 语句内部,接着再次判断instance是否为空,为空就去new Singleton对象并且赋值给instance,A线程退出 synchronized 语句,交出同步锁,B线程进入 synchronized 语句内部,if判断instance是否为空,防止创建不同的instance对象,这也是第二个if判断的作用,B线程发现不为空,所以直接退出,所以最终A和B线程可以获取到同一个Singleton对象,之后的线程调用getInstance()函数,都会因为Instance不为空而直接返回,不会受到 synchronized 的性能影响。
volatile关键字介绍
double-checked方法用到了volatile关键字,volatile关键字的作用需要仔细介绍一下,在C/C++中,volatile关键字的作用和java中是不一样的,总结一下:
- C/C++中的volatile关键字作用
- 可见性“可见性”指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。
- 不可优化性“不可优化”特性,volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
- 顺序性”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。Volatile变量与非Volatile变量的顺序,编译器不保证顺序,可能会进行乱序优化。同时,C/C++ Volatile关键词,并不能用于构建happens-before语义,因此在进行多线程程序设计时,要小心使用volatile,不要掉入volatile变量的使用陷阱之中。
- (适用于Java所有版本)读和写一个volatile变量有全局的排序。也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。
- (适用于Java5及其之后的版本)volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁[8]。
上面有一个细节,java 5版本之后volatile的读与写才建立了一个的关系,之前的版本会出现一个问题:,这个答案写的很清楚了,线程 A 在完全构造完 instance 对象之前就会给 instance 分配内存,线程B在看到 instance 已经分配了内存不为空就回去使用它,所以这就造成了B线程使用了部分初始化的 instance 对象,最后就会出问题了。里面有一句话
As of J2SE 5.0, this problem has been fixed. The volatile keyword now ensures that multiple threads handle the singleton instance correctly. This new idiom is described in [2] and [3].复制代码
所以对于 android 来说,使用 volatile关键字是一点问题都没有的了。
参考文章:eager initialization thread-safety (饿汉法,线程安全)
“饿汉法”就是在使用该变量之前就将该变量进行初始化,这当然也就是线程安全的了,写法也很简单:
private static Singleton instance = new Singleton();private Singleton(){ name = "eager initialization thread-safety 1";}public static Singleton getInstance(){ return instance;}复制代码
或者
private static Singleton instance = null;private Singleton(){ name = "eager initialization thread-safety 2";}static { instance = new Singleton();}public Singleton getInstance(){ return instance;}复制代码
代码都很简单,一个是直接进行初始化,另一个是使用静态块进行初始化,目的都是一个:在该类进行加载的时候就会初始化该对象,而不管是否需要该对象。这么写的好处是编写简单,而且是线程安全的,但是这时候初始化instance显然没有达到lazy loading的效果。
static inner class thread-safety (静态内部类,线程安全)
由于在java中,静态内部类是在使用中初始化的,所以可以利用这个天生的延迟加载特性,去实现一个简单,延迟加载,线程安全的单例模式:
private static class SingletonHolder{ private static final Singleton instance = new Singleton();}private Singleton(){ name = "static inner class thread-safety";}public static Singleton getInstance(){ return SingletonHolder.instance;}复制代码
定义一个 SingletonHolder 的静态内部类,在该类中定义一个外部类 Singleton 的静态对象,并且直接初始化,在外部类 Singleton 的 getInstance() 方法中直接返回该对象。由于静态内部类的使用是延迟加载机制,所以只有当线程调用到 getInstance() 方法时才会去加载 SingletonHolder 类,加载这个类的时候又会去初始化 instance 变量,所以这个就实现了延迟加载机制,同时也只会初始化这一次,所以也是线程安全的,写法也很简单。
PS
上面提到的所有实现方式都有两个共同的缺点:
- 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
- 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
enum (枚举写法)
JDK1.5 之后加入 enum 特性,可以使用 enum 来实现单例模式:
enum SingleEnum{ INSTANCE("enum singleton thread-safety"); private String name; SingleEnum(String name){ this.name = name; } public String getName(){ return name; }}复制代码
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。但是很不幸的是 android 中并不推荐使用 enum ,主要是因为在 java 中枚举都是继承自 java.lang.Enum 类,首次调用时,这个类会调用初始化方法来准备每个枚举变量。每个枚举项都会被声明成一个静态变量,并被赋值。在实际使用时会有点问题,这是 google 的官方文档介绍:
这篇博客也专门计算了 enum 的大小:,所以枚举写法的缺点也就很明显了。登记式
登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
//类似Spring里面的方法,将类名注册,下次从里面直接获取。 public class Singleton { private static Map map = new HashMap(); static{ Singleton single = new Singleton(); map.put(single.getClass().getName(), single); } //保护的默认构造子 protected Singleton(){} //静态工厂方法,返还此类惟一的实例 public static Singleton getInstance(String name) { if(name == null) { name = Singleton.class.getName(); System.out.println("name == null"+"--->name="+name); } if(map.get(name) == null) { try { map.put(name, (Singleton) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); } //一个示意性的商业方法 public String about() { return "Hello, I am RegSingleton."; } public static void main(String[] args) { Singleton single3 = Singleton.getInstance(null); System.out.println(single3.about()); } } 复制代码
这种方式我极少见到,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。
总结
综上所述,平时在 android 中使用 double-checked 或者 SingletonHolder 都是可以的,毕竟 android 早就不使用 JDK5 之前的版本了。由于 android 中的多进程机制,在不同进程中无法创建同一个 instance 变量,就像 Application 类会初始化两次一样,这点需要注意。
但是不管采取何种方案,请时刻牢记单例的三大要点:- 线程安全;
- 延迟加载;
- 序列化与反序列化安全。
- 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现;
- 单例对象如果持有 Context,那么很容易引发内存泄漏,此时需要注意传递给单例对象的 Context 最好为 Application Context。
Rules of thumb
有些时候是可以重叠使用的,有一些和都可以使用的场景,这个时候使用任一设计模式都是合理的;在其他情况下,他们各自作为彼此的补充:可能会使用一些原型类来克隆并且返回产品对象。
,和都能使用来实现他们自己;经常也是通过实现的,但是他们都能够使用来实现; 通常情况下,设计模式刚开始会使用(结构清晰,更容易定制化,子类的数量爆炸),如果设计者发现需要更多的灵活性时,就会慢慢地发展为,或者(结构更加复杂,使用灵活); 并不一定需要继承,但是它确实需要一个初始化的操作,一定需要继承,但是不一定需要初始化操作; 使用或者的情况通常也可以使用来获得益处; 中,只要将构造方法的访问权限设置为 private 型,就可以实现单例。但是的 clone 方法直接无视构造方法的权限来生成新的对象,所以,与是冲突的,在使用时要特别注意。源码下载