对于一个软件系统中的某些类来说,只有一个实例很重要。例如在Windows操作系统中就只能打开一个任务管理器窗口,如果不对窗口进行唯一化,势必会弹出多个窗口。如果这些窗口显示的内容完全一致,那么这些窗口就是重复对象,白白浪费内存资源;如果这些窗口显示的内容不一致,那么意味着操作系统在某个时刻上存在多个不同的状态,很显然这是与实际不相符合的,还会给用户带来误解,无法确定哪一个窗口才是系统最真实的状态。因此对象的唯一性在某种情况下非常重要。
定义:
Ensure a class has a only one instance, and provide a global point of access to it.
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
那么如何才能保证一个类只有一个实例并且易于外部访问呢?
首先,构造方法私有化。只有构造方法私有化,让类自身负责创建实例对象,防止外部进行创建对象,从根本上解决出现多个对象实例的情况;
其次,还要定义一个静态私有成员变量来保存该类的实例对象;
最后,由于构造方法私有化了,外部无法访问该类的实例,因此类中还要有一个供外部访问该类实例的方法;
单例模式实现:
public class Singleton {
//静态私有成员变量
private static Singleton instance = null;
//私有构造函数
private Singleton() {
}
//静态方法,返回唯一实例
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
为了验证单例类创建的对象确实是唯一的,编写以下测试代码进行测试:
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
if (s1 == s2) {
System.out.println("两个对象是同一个实例");
} else {
System.out.println("两个对象是不同的实例");
}
}
/*输出结果:
两个对象是同一个实例
*/
从上面单例模式的实现来看,在第一次调用 getInstance()
方法时才进行对象的实例化,在类加载时并不自行实例化,这种技术又称为 延迟加载技术(Lazy Load)
。为了避免多个线程同时使用getInstance()出现异常,可以使用 synchronized
关键字进行修饰。
//静态方法,返回唯一实例
public static synchronized Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
上述代码虽然解决了线程安全问题,但是每次调用 getInstance()
方法都需要进行线程锁定判断,在多线程高并发访问环境中将会导致系统性能大打折扣,因此可以改进为只对创建对象的代码进行锁定,改进后如下:
//静态方法,返回唯一实例
public static Singleton getInstance(){
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
粗略一看问题貌似得到解决,但实际上并不是这样的,改进后的代码还是可能存在对象不唯一的情况。原因如下:假设在某一瞬间线程A和B都在调用 getInstance()
方法,此时 instance
对象为 null 值,均能通过 instance == null
的判断,由于实现了 synchronized 加锁机制,线程A进入锁定的代码中执行创建实例的代码,线程B处于排队等待状态,必须等待线程A执行完毕之后才可以进入加了锁的代码块。但当线程A执行完毕之后,线程B并不知道实例已经创建,将会继续执行创建实例的代码,导致产生多个对象,这就违背了单例模式的设计思想,因此需要进行改进。改进后完成代码如下:
public class Singleton {
//静态私有成员变量
private volatile static Singleton instance = null;
//私有构造函数
private Singleton() {
}
//静态方法,返回唯一实例
public static Singleton getInstance(){
//一重判断
if (instance == null) {
//锁定代码块
synchronized (Singleton.class) {
//二重判断
if (instance == null) {
//创建单例实例
instance = new Singleton();
}
}
}
return instance;
}
}
在锁定代码块种在进行一次 instance == null 判断,这种方式称为双重检查锁定(Double-Check Locking)。
需要注意的是,使用双重检查锁定来实现单例类,需要在静态成员变量 instance 之前增加修饰符 volatile
,被它修饰的成员变量可以确保多个线程都能够正确处理,且只能在JDK1.5 以上版本才能正确执行。
上述代码的实现方式称为懒汉式单例,懒汉式单例类在第一次被引用时将自己实例化,在类加载时不会将自己实例化。
饿汉式单例:
还有一种饿汉式单例类实现方法,它的特点是当类被加载时,静态变量 instance 就会被初始化,此时类的私有构造函数会被调用,单例的唯一实例被创建。
public class EagerSingleton {
//静态私有变量
private static final EagerSingleton instance = new EagerSingleton();
//私有构造方法
private EagerSingleton() {
}
//静态方法返回唯一实例
public static EagerSingleton getInstance() {
return instance;
}
}
饿汉式单例类与懒汉式单例类的比较:
1.饿汉式单例类在类加载时就实例化对象,无须考虑多线程同时访问的问题,可以确保实例的唯一性;
同时,由于对象一开是就得以创建,还在调用速度和反应时间上有一定优势;
类加载时间较懒汉式可能会更长,并且一旦用不到该对象实例,资源利用率也不及懒汉式单例类。
2.懒汉式单例类实现了延迟加载,但是必须处理多个线程同时访问的问题;
当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能需要大量时间,
这意味着出现多线程同时首次引用该类的几率变得更大,需要通过双重检查锁等机制进行控制,这将对系统的性能产生一定的影响。
使用静态内部类实现单例模式:
无论是饿汉式单例类还是懒汉式单例类,它们都存在一定的不足。为了克服这些不足,在Java语言中可以通过 Initialization on Demand Holder(IoDH)
技术来实现单例模式。
在使用该技术中,需要在单例类中添加一个静态内部类,在该内部中创建单例对象,再将该对象通过方法返回给外部进行调用。
public class IoDHSingleton {
private IoDHSingleton() {
}
//静态内部类
private static class HolderClass {
private static final IoDHSingleton instance = new IoDHSingleton();
}
public static IoDHSingleton getInstance() {
return HolderClass.instance;
}
}
由于静态单例对象没有作为 IoDHSingleton 的成员变量直接实例化,因此类加载时不会实例化 IoDHSingleton,第一次调用 getInstance() 时将加载内部类 HolerClass,在该内部中定义了一个静态类型的变量 instance ,此时会首先初始化这个成员变量,由 Java 虚拟机来保证其线程安全性,确保该成员变量只能初始化一次;由于 getInstance() 方法没有线程锁定,因此其性能不受影响。
补充:
1.静态成员变量和静态代码块只有在类被调用的时候才会初始化
2.静态内部类只有当被外部类调用到的时候才会初始化
单例模式优点:
1.提供了对唯一实例的受控访问
2.节约系统资源,提高系统性能
缺点:
1.单例类的扩展很困难
2.单例类职责过重,违背了单一职责原则
3.如果实例化的共享对象长时间不被利用,自动回收机制可能会识别为垃圾进行回收,导致共享的单例对象状态的丢失