关于设计模式的学习笔记,教材:《设计模式的艺术之道》 刘伟 著
class TaskManager{
private static TaskManager tm=null;
private TaskManager(){
//大量初始化工作
}
public static TaskManager getInstance(){
if(tm==null){
tm=new TaskManager();
}
return tm;
}
}
一般的单例模式:
- 将构造函数私有
- 定义静态的TaskManager类型私有成员变量tm
- 增加一个公有的静态方法,返回tm
但这样会出现问题。
比如当多次调用getInstance()方法,而new TaskManager()时因为大量初始化工作需要消耗不少时间时,由于tm尚未创建成功,于是其他调用的getInstance()方法中,tm仍为null,可能会导致创建了多个tm对象。违背了单例模式的初衷。
一开始人们想到两种解决方法:
1.Eager Singleton
在类加载的时候就已经创建单例对象
class TaskManager{
private static final TaskManager tm=new TaskManager();
//其余同上
}
2.Lazy Singleton
在类加载时并不实例化,而是在需要时再加载,这种技术又称为Lazy Load(延迟加载技术)。为了避免多个线程同时调用getInstance()方法,在Java中使用关键字synchronized
class TaskManager{
synchronized public static TaskManger getInstance(){
if(tm==null){
tm=new TaskManager();
}
return tm;
}
}
通过增加关键字synchronized进行线程锁定,以处理多个线程同时访问的问题。但是在多线程高并发访问环境下,会导致系统性能大大降低。比如当tm已经创建,而同时调用多次getInstance(),实际上不会并发执行,实际上只需直接return,但要排队。
于是有了这个改进:
class TaskManager{
public static TaskManger getInstance(){
if(tm==null){
synchronized (TaskManager.class){
tm=new TaskManager();
}
}
return tm;
}
}
似乎问题解决了,但是如果在某一瞬间,线程A和B都在调用getInstance(),此时均能通过tm==null的判断,那么依然new了两次。因此需要进一步改进。在synchronized 锁定代码中再进行一次tm==null的判断,这称为Double-Check Locking。
class TaskManager{
private volatile static final TaskManager tm=null;
public static TaskManger getInstance(){
if(tm==null){
synchronized (TaskManager.class){
if(tm==null){
tm=new TaskManager();
}
}
}
return tm;
}
}
一开始我有个疑惑,外面的if(tm==null)
是否可以忽略?但仔细一想,如果忽略,那就跟最开始把整个方法都synchronized 差不多了。
这种Double-Check Locking需要在静态成员变量tm添加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能正确处理。(jdk1.5以上)但是由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用Double-Check Locking依然不好。
比较两种方法
Eager无须考虑多线程访问时的问题,而且因为一开始就实例化好,所以调用速度和反应时间由于Lazy。但是无论系统在运行时是否需要使用该单例对象,在类加载时都创建好了,就会消耗更多系统资源,而且加载时间可能会比较长。
关于类的加载:http://blog.csdn.net/ns_code/article/details/17881581
而Lazy无须一直占用系统资源,但是必须处理好多线程同时访问问题,多线程同时首次引用此类时需要双重检查锁定等机制进行控制,将导致系统性能受到影响。
一种更好的方法
class TaskManager{
private TaskManager(){}
private static class HolderClass{
private static final TaskManager tm=new TaskManager();
}
public static TaskManger getInstance(){
return HolderClass.tm;
}
}
类加载时没有实例化TaskManager,而第一次调用getInstance()时加载内部类,初始化其成员变量tm,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。
这种方法叫Initialization on Deman Holder技术。
通过IoDH,既可以实现Lazy Load,减少系统资源消耗,又保证线程安全,不影响系统性能。缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH。
单例模式的优缺点
优点
- 提供了对唯一实例的受控访问
- 节约系统资源
- 允许可变数目的实例
缺点
- 没有抽象层,类的拓展有很大困难
- 职责过重,即提供业务方法,又提供了创建对象的方法,即既有对象的创建,也有对象的使用
- 很多语言如Java、C#的运行环境都提供了自动垃圾回收技术,因此,如果实例化的单例对象长时间不被使用,系统会认为是垃圾,自动销毁并回收。下次使用时重新实例化,这将导致共享的实力对象状态丢失。
适用场景
- 系统只需要一个实例对象
- 客户调用类的单个实例只允许使用一个公共访问点。