单例模式是一种常用的软件设计模式,通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问。
1、使用场景
在某些系统中某些对象最多只能存在一个,例如Windows中只能打开一个任务管理器,一个系统只能有一个计时工具或序号生成器,此时,建议使用单例模式。
2、要点
1) 单例模式的类只提供私有的构造函数;
2) 类定义中含有一个该类的静态私有对象;
3) 类提供了一个静态的共有的函数用户创建或获取它本身的静态私有对象;
1、懒汉模式
所谓的懒汉模式其实是一种比较形象的称谓,他的懒主要表现是在类装载的时候并不着急创建对象实例,等到需要实例时再去创建,是懒加载的一种表现。懒汉模式是一种典型的以时间换空间的做法,在每次获取实例的时候都会进行判断,看是否需要创建实例,浪费了每次判断的时间,优点是没人使用实例时可节约内存空间。
1) 线程不安全的懒汉模式。优点:具备lazy loading 缺点:不支持多线程 推荐指数:不推荐
public class Singleton {
private static Singleton instance;
//私有构造函数 防止类在外部被实例化
private Singleton(){}
//静态工厂方法
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
这种单例模式是初学者最先想到的一个设计模式,获取实例时先执行判断,在单线程程序中可能是一个不错的设计模式,但是在多线程程序中,它无法正常工作,因此不推荐使用。
2) 线程安全的懒汉模式。优点:具备lazy loading,支持多线程 缺点:多线程下每次获取实例都需同步,效率很低 推荐指数:★
public class Singleton {
private static Singleton instance;
//私有构造函数 防止类在外部被实例化
private Singleton(){}
//静态工厂方法
public static synchronized Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
相比上一种设计模式,增加了关键字synchronized,可以保证该设计模式在多线程下能够工作而不发生混乱。但在多线程工作环境中,每个线程调用方法getInstance都会同步,因此会大大降低程序执行效率。
2、饿汉模式
所谓的饿汉模式是一种比较形象的称谓,因为饿,所以在类初次装载时就着急创建了类的实例。饿汉模式是一种典型的以空间换时间的做法,当类装载时就会创建类的实例,不管你装载类的时候是否使用实例,他都会创建出来,此后获取实例时就无需判断是否存在,节省了运行时间,且能够良好地支持多线程环境。缺点是,在理论上当实例对象很大时,过早地创建实例可能会浪费一定的内存空间,但笔者认为,在通常情况下无需考虑这种情况。
标准饿汉模式。优点:编写简单,良好地支持多线程且运行效率很高 缺点:不具备lazy loading,因此理论上在某种情况下会浪费一定的内存空间 推荐指数:★★★★
public class Singleton {
//写法1
private static Singleton instance = new Singleton();
//写法2
//private static Singleton instance = null;
//static{
// instance = new Singleton();
//} //私有构造函数 防止类在外部被实例化
private Singleton(){}
//静态工厂方法
public static Singleton getInstance(){
return instance;
}
}
我们知道,当整个程序运行时,静态类Singleton类并不会立即被装载,因此其实例也不会立即被初始化。只有在某处使用到了Singleton类时,它才会被装载。因此在理论上,饿汉模式的唯一缺点成立需要以下两个条件:①类对象非常占用内存 ②类体内除了方法getInstance还有其他静态公有方法或静态变量,且这些方法或变量需在调用方法getInstance前使用。在上述两个条件均成立时,饿汉模式所表现出的缺点也仅是在一定时间内浪费了内存空间,这里的时间间隔指的是从调用条件②中的方法或变量开始到调用方法getInstance的时间间隔。综上,这种单例模式编写简单且能够在多线程环境中高效地运行,笔者认为近乎完美,如果不是过分追求设计360度无死角,可以毫不夸张地说,饿汉模式适用于任何条件。
3、 双重校验锁
双重校验锁是在线程安全的懒汉模式的一个升级版,由于线程安全的懒汉模式在多线程环境下运行性能较低,双重校验锁在上述基础上进行改进,只在类实例未初始化时加锁初始化过程,待类实例初始化后,获取类实例并不会执行同步操作,因此大大提高了执行效率。
双重校验锁模式。优点:具备lazy loading节省空间,良好地支持多线程且运行效率较高 缺点:编写稍复杂,volatile关键字会影响运行效率 推荐指数:★★★
public class Singleton {
//注意此处关键字volatile,使用该关键字,变量instance将不会被本地线程缓存,所有对该变量的读写操作都是直接操作共享内存,从而确保该变量的值最新且正确
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
//先检查实例是否存在,如果不存在才会进入下面的同步代码块
if(instance==null){
synchronized(Singleton.class){
//需再次检查实例是否存在,如果不存在才真正创建
//有可能一个线程获取instance时为null,但进入当前同步代码块后instance已经被其他线程创建,因此需判断是否重复
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
注意,这里必须使用volatile关键字,因为instance = new Singleton()不是一个原子操作,实际执行时可能被指令重排,导致外部获取到初始化不完整的instance。
这种设计模式既可以实现线程安全地创建实例,而又不会对性能造成很大的影响,仅仅在初始化类实例时需要同步,此后获取实例都不需要同步操作。在上述代码实现中,可以看到使用了关键字volatile,被该关键字修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。但是,由于volatile关键字可能会屏蔽虚拟机中一些必要的代码优化,所以会在一定程序上影响运行效率,因此对于双重校验锁模式,并不建议大量使用。
4、静态内部类
静态内部类是饿汉模式的升级版,由于饿汉模式理论上在某些条件下存在缺点,该缺点产生的原因是类体内除了getInstance方法还存在其他公有静态方法或变量,在调用这些方法或变量时,类会被装载,同时类实例也会被立即初始化,我们可以将Singleton的实例存放于一个内部类SingletonHolder中,只有当调用Singleton获取实例对象方法时,才会装载内部类SingletonHolder,很好地解决了饿汉模式存在的缺点。
静态内部类模式。优点:具备lazy loading节省空间,完美地支持多线程且运行效率很高 缺点:暂无 推荐指数:★★★★★
public class Singleton {
//私有构造函数
private Singleton(){}
//类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
private static class SingletonHolder{
//静态初始化器,由JVM来保证线程安全
private static Singleton instance = new Singleton();
}
//静态工厂方法
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
相比上述其他单例模式的实现方式,静态内部类实现方法具备他们拥有的任何优点(lazy loading懒加载,支持多线程并且高效),因此,静态内部类是一种比饿汉模式更完美的单例实现方式。
5、枚举
在《高效Java第二版》中,作者提到了使用枚举类型来实现单例模式,实现起来更加简洁、高效和安全,防止了多次实例化,给出的示例代码如下所示。
public enum Singleton {
//定义一个枚举的元素,它就代表了Singleton的一个实例。
uniqueInstance; //单例可以有自己的操作
public void singletonOperation(){
//功能处理
}
}
对于这种实现方式,写法超级简单,又能解决大部分的问题。