设计模式之单例模式
目的:希望对象只创建一个实例,并且提供一个全局的访问点。
使用场景: 要求只能有一个实例。比如打印机服务。多个电脑或其他设备共享一个打印机,但同一时间只能有一个输出。
还有序列生成器,整个应用必须是唯一的,只能有一个实例;windows任务管理器和回收站等。
增加了synchronized关键字后,会影响性能。多线程时大部分时间都用来synchronized关键字的准备上。所以有了double check lock(双重锁定检查)
我们来看看这个场景:假设线程一执行到instance = new SingletonC()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。
事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给SingletonC的实例分配内存。
2.初始化SingletonC的构造器
3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,
如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,
所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来。
如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonC instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。
还有一种实现单例的方法是采用ThreadLocal。
目的:希望对象只创建一个实例,并且提供一个全局的访问点。
使用场景: 要求只能有一个实例。比如打印机服务。多个电脑或其他设备共享一个打印机,但同一时间只能有一个输出。
还有序列生成器,整个应用必须是唯一的,只能有一个实例;windows任务管理器和回收站等。
/**
* 饿汉式单例,类加载的时候就初始化
*
*/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
/**
* 懒汉式单例,只有第一次调用的时候才初始化
*/
public class SingletonA {
private static SingletonA instance = null;
private SingletonA(){
}
public static SingletonA getInstance(){
if(instance==null){
instance = new SingletonA();
}
return instance;
}
}
/**
* 懒汉式单例,只有第一次调用的时候才初始化。线程安全控制,采用synchronized关键字
*/
public class SingletonB {
private static SingletonB instance = null;
private SingletonB(){
}
public synchronized static SingletonB getInstance(){
if(instance==null){
instance = new SingletonB();
}
return instance;
}
}
增加了synchronized关键字后,会影响性能。多线程时大部分时间都用来synchronized关键字的准备上。所以有了double check lock(双重锁定检查)
/**
* 懒汉式单例,只有第一次调用的时候才初始化。线程安全控制,采用DCL(double check lock)
*/
public class SingletonC {
private static SingletonC instance = null;
private SingletonC(){
}
/**
* DCL(双重锁定检查),可很大程度上减轻因同步带来的性能消耗,只在instance为null的时候才同步。
*
*/
public static SingletonC getInstance(){
if(instance==null){
synchronized(SingletonC.class){
if(instance==null){
instance = new SingletonC();
}
}
}
return instance;
}
}
我们来看看这个场景:假设线程一执行到instance = new SingletonC()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。
事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给SingletonC的实例分配内存。
2.初始化SingletonC的构造器
3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,
如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,
所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来。
如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonC instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。
还有一种实现单例的方法是采用ThreadLocal。
/**
* 采用ThreadLocal实现单例,每个线程有一个单例
*/
public class SingletonD {
private static ThreadLocal<SingletonD> singletonDHolder = new ThreadLocal<SingletonD>();
private SingletonD(){
}
public static SingletonD getInstance(){
if(singletonDHolder.get()==null){
singletonDHolder.set(new SingletonD());
}
return singletonDHolder.get();
}
}