单例模式(Singleton Pattern)(一)

2014-11-24 03:00:29 · 作者: · 浏览: 2
目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
其实单例模式应用很多,我也不陌生,有时候一些自己定义的Controller等,都会选择单例模式去实现,而本身java.lang.Runtime类的 源码也使用了单例模式(Jdk7u40):
复制代码
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
  return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
......
}
复制代码
然而,因为涉及到多线程 编程,单例模式还是有不少值得注意的地方,请看下面的各种实现。
1.最简单实现:
复制代码
/**
* @author YYC
* lazy-loading but NOT thread-safe
*/
public class SingletonExample {
private static SingletonExample instance;
private SingletonExample(){}
public static SingletonExample getInstance(){
  if(instance==null){
  instance = new SingletonExample();
  }
  return instance;
}
}
复制代码
这是单例模式最简单最直接的实现方法。懒汉式(lazy-loading)实现,但缺点很明显:线程不安全,不能用于多线程环境
2.同步方法实现:
复制代码
/**
* @author YYC
* Thread-safe but bad performance
*/
public class SingletonExample {
private static SingletonExample instance;
private SingletonExample(){}
public static synchronized SingletonExample getInstance(){
  if(instance==null){
  instance = new SingletonExample();
  }
   return instance;
}
}
复制代码
同步getInstance()这个方法,可以保证线程安全。不过代价是性能会受到,因为大部分时间的操作其实不需要同步。
3. Double-Checked Locking实现(DCL):
复制代码
/**
* @author YYC
* Double-Checked Locking
*/
public class SingletonExample {
private static SingletonExample instance;
private SingletonExample(){}
public static SingletonExample getInstance(){
  if(instance==null){
  synchronized(SingletonExample.class){
    if(instance==null){
    instance = new SingletonExample();
    }
  }
  }
  return instance;
}
}
复制代码
直接同步整个getInstance()方法产生性能低下的原因是,在判断(instance==null)时,所有线程都必须等待。而(instance==null)并非是常有情况,每次判断都必须等待,会造成阻塞。因此,有了这种双重检测的实现方法,待检查到实例没创建后(instance=null),再进行同步,然后再检查一次确保实例没创建。
在同步块里,再判定一次,是为了避免线程A准备拿到锁,而线程B创建完instance后准备释放锁的情况。如果在同步块里没有再次判定,那么线程A很可能会又创建一个实例。
另外,再引用IcyFenix文章里面的一段话,会解释清楚双锁检测的局限性:
我们来看看这个场景:假设线程一执行到instance = new SingletonExample()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给SingletonExample的实例分配内存。
2.初始化SingletonExample的构造器
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,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。
DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonExample instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL