单例模式(一)

designPattern_sigleten
单例模式是最简单最常见的设计模式之一。单例模式的学习教程一般都会提到饿汉式、懒汉式,其中体现了单例模式最要关心的两个问题:对象创建的次数以及何时被创建。 本文会着重总结如上两类实现方式。

介绍

单例模式,即该类的对象只允许一个实例存在,提供全局的访问。既然只允许进行一次实例化,那么比较容易想到的实现方式就是将构造函数设为私有,然后类内创建一个对象,并对外提供一个接口用于使用者的获取。但由于多线程以及资源、效率等方面的考虑,衍生出多种实现方式。

应用场景

乍一听可能觉得单例模式局限性太大,用处有限,其实在实际的应用场景有很多:

  1. 工具类对象,如应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。如log4j日志系统。又比如windows的任务管理器,回收站。

  2. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

  3. 需要频繁创建销毁的对象,或创建对象时耗时过多或耗费资源过多,但又经常用到的对象。数据库连接池的设计一般就是采用单例模式,因为数据库连接是一种数据库资源,数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的。

  4. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

  5. spring中的bean默认也是单例的。

  6. ……(写的有些乱,不过应该还有很多)

实现方式

所谓的饿汉式、懒汉式,我觉得应该是按照是否实现了延迟加载(lazy load)来区分的。最好的情况是即实现了lazy load,保证了线程安全,同时又能在性能(如运行时间)上有一定的保证。

1.饿汉式(不保证线程安全)

public class Singleton{
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton genInstance(){
return INSTANCE;
}
}

这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。但如果从始至终从未使用过这个实例,则会造成内存的浪费。

2.懒汉式

版本一:基本版本(不保证线程安全)

public class Singleton{
private static Singleton INSTANCE = null;
private Singleton(){}
public static Singleton genInstance(){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

实现了Lazy Load,即第一次使用时才进行实例化,但仅能在单线程程序中使用,多线程无法保证单例。如果在创建实例对象前,两个线程都通过了if的判断,则会new出两个对象。

版本二:synchronized同步方法(保证线程安全,不推荐用)

public class Singleton{
private static Singleton INSTANCE = null;
private Singleton(){}
public static synchronized Singleton genInstance(){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

实现了Lazy Load,且能保证线程安全。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上说很不划算。

(synchronized不仅实现了实现线程间的互斥,还能保证内存的可见性:由Synchronized的内存可见性说起)

版本三:synchronized同步块(不保证线程安全,无法使用,用于比较)

public class Singleton{
private static volatile Singleton INSTANCE = null;
private Singleton(){}
public static Singleton genInstance(){
if(INSTANCE==null){
synchronized(Singleton.class){
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}

由于上一个版本同步效率太低,所以摒弃同步方法,改为在同步代码块内完成实例化。但是这种同步并不能保证线程安全。与版本一的情形一致,假如一个线程进入了if判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例对象。

版本四:双检锁(保证线程安全,推荐)

public class Singleton{
private static volatile Singleton INSTANCE = null;
private Singleton(){}
public static Singleton genInstance(){
if(INSTANCE==null){
synchronized(Singleton.class){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

双检锁(DCL,double check lock)即双重if检查,同时在使用synchronized代码块的同时,还要将INSTANCE的定义设为volatile类型。

volatile(java5及以后版本)可以保证多线程下变量的可见性:

读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。

写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。

对于双检锁的具体论述,可以参考单例模式中用volatile和synchronized来满足双重检查锁机制

版本五:内部静态类(保证线程安全,推荐)

public class Singleton {
private Singleton() {}
private static class SingletonInner {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.INSTANCE;
}
}

内部静态类SingletonInner并不会随着Singleton类的装载而装载,要在有SingletonInner的引用了以后才会装载到内存的。所以在第一次调用getInstance()之前,SingletonInner是没有被装载进来的,只有在第一次调用了getInstance()之后,里面调用了内部静态类的静态方法,产生了对SingletonInner的引用,内部静态类才会真正装载。从而产生实例对象,这也就达到了Lazy load的目的。

而类的静态属性只会在第一次加载类的时候初始化,而且在类装载时,别的线程是无法进入的。所以在这里,JVM帮助我们保证了线程的安全性。

其他

至于枚举实现、反射、序列化等相关问题,将在下一部分进行总结。

参考(看了很多,主要是以下两个):

单例模式的八种写法比较
单例模式的八种写法比较、枚举实现的好处、静态内部类实现单例原理

------ 本文结束 ------