1、单例模式介绍
单例模式是应用最广的模式之一,也是很多初级工程师唯一会使用的设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在。如一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中有包含线程池、缓存系统、网络请求等,很消耗资源,因此,这种不能自由构造多个对象的情况。’
2、定义
确保某一类只有一个实例,而且自行实例化并向整个系统提供这个实例。
3、使用场景
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。如要访问IO和数据库资源。
4、单例模式的关键点
(1)构造函数不对外开放,一般为private
(2)通过一个静态方法或者枚举返回单例类对象
(3)确保单例类的对象有且只有一个,尤其是在多线程环境下
(4)确保单例类对象在反序列化是不会重新构建对象
5、单例模式的分类
(1)懒汉模式
懒汉模式时声明一个静态对象,并且在用户第一次使用getInstance时进行初始化
public class LazySingleton {
//懒汉式单例模式
//比较懒,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
private static LazySingleton intance = null;//静态私用成员,没有初始化
private LazySingleton(){
//私有构造函数
}
public static synchronized LazySingleton getInstance(){ //静态,同步,公开访问点
if(intance == null){
intance = new LazySingleton();
}
return intance;
}
}
注:getInstance()方法中添加了synchronized关键字,也就是getInstance是一个同步方法,这种保证了单例对象的唯一
懒汉单例模式的优点是单例只有在使用时才会被实例化,在一定程度上节约了资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,醉倒的问题是每次调用getInstance都是进行同步,造成不要的同步开销,不太建议使用。
(2)饿汉模式
在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
public class EagerSingleton {
//饿汉单例模式
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化
private EagerSingleton() {
//私有构造函数
}
public static EagerSingleton getInstance(){ //静态,不用同步(类加载时已初始化,不会有多线程的问题)
return instance;
}
}
(3)Double Check Lock(DCK)实现单例
DCL方式实现单例模式的优点是技能够在需要时才初始化单例,又能保证了线程安全,且单例对象初始化后调用getInstance不进行同步锁。
public class Singleton{
private static Singleton sInstance=null;
private Singleton(){
//私有构造函数
}
public void doSomething(){
System.out.println("do sth.");
}
public static Singleton getInstance(){
if(sInstance==null){
synchronized(Singleton.class){
if(sInstance==null){
sInstance=new Singleton();
}
}
}
return sInstance;
}
}
可以看出getInstance()方法中对instance进行了两次判空,第一层判断主要是威力避免不必要的同步,第二层判断则是为了在null的情况下创建实例。这是什么意思呢?下面来分析一下:
假设线程A执行到sInstance=new Singleton()语句,这里看起来是一句代码,但实际上他并不是一个原子操作,这句话最终会被编译成多条汇编指令,大致做了3件事:
(1)给Singleton的实例分配内存
(2)调用Singleton()的构造幻术,初始化成员字段
(3)将Instance对象指向分配的内存空间(此时sInstance就不是null了)
但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Modle,即java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的,也就是说,执行顺序可能进是1-2-3,也可能是1-3-2.如果是后者,并且在3执行完毕,2未必执行之前,别切换到线程B上,这时候sInstance因为已经在线程A内执行过了第三点,sInstance已经是非空了,所以,线程B直接取走sInstance,再使用是就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的错位很可能会隐藏很久。
在JDK1.5之后,SUN官方已经注意到这个问题,调整了JVM,具体化了volatile关键字,因此,如果JDK是1.5或之后的版本,只需要将sInstance的定义改成private volatile static Singleton sInstance=null 就可以保证sInstance对象每次都是从内存中读取,就可以使用DCL的写法来完成单例模式,当然,volatile或多或少也会影响到性能,但是考虑到正确性,牺牲这点性能还是值得的。
DCL优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点:第一次加载时反应稍慢,也由于Java内存模型的原因偶尔会失败。
(4)静态内部类模式
public class Singleton {
private Singleton(){ }
public static Singleton getInstance() {
return Nested.instance;
}
//静态内部类 在第一次被引用时被加载
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
public static void main(String args[]){
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}
当第一次加载Singleton类是并不会初始化sInstance,只有在第一次打调用Singleton的getInstance()方法是才会导致sInstance被初始化,因此,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder类,这种方法不仅能够确保线程安全,也能保证单例对象的唯一性,同事也延迟了单例的实例化,所以这是推进使用的单例模式实现方式。
总结:使用单例模式时要注意