顾名思义,lazy loading(延迟加载,一说懒加载),在需要的时候才创建单例对象,而不是随着软件系统的运行或者当类被加载器加载的时候就创建。当单例类的创建或者单例对象的存在会消耗比较多的资源,常常采用lazy loading策略。这样做的一个明显好处是提高了软件系统的效率,节约内存资源。下面我们看看最简单的懒汉单例模式:
public class Singleton {
private static Singleton singleton = null ;//自己内部维护一个私有的实例变量
//构造方法设置为私有,防止外部实例化该对象,破坏单例
private Singleton(){
System.
out
.println(
"构造函数被调用"
);
}
//公有静态方法,供外部调用来获取单例对象
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
在单线程环境下,多次调用getInstance()方法获得的Singleton对象均为同一个对象,单例模式实现成功。然而,在更多时候,软件系统工作于多线程环境下,因此不得不考虑线程安全的问题。
现有多线程测试程序如下:
public class TestThread {
public static void main(String[] args) {
Runnable runnable = new Runnable(){
public void run() {
Singleton.getInstance();
};
};//创建实现了Runnable 接口的匿名类
for(int i=0;i<1000;i++){
new Thread(runnable).start();
}
}
}
显然,Singleton的构造方法不止一次被调用,也就是说,Singleton存在三个实例对象,这违背了单例模式的初衷。由此表明,简单的懒汉式在多线程环境下不是线程安全的。在多线程环境下我们遇到线程不安全问题肯定会先想到加锁,那我们应该在哪里加锁呢?我们可以给整个方法加锁,但是这样粒度会变大,不利于效率,那么锁代码呢?接下来我们就做个实验:
public class Singleton {
private static Singleton singleton = null ;//自己内部维护一个私有的实例变量
//构造方法设置为私有,防止外部实例化该对象,破坏单例
private Singleton(){
System.out.println("构造函数被调用");
}
//公有静态方法,供外部调用来获取单例对象
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){ //加锁
singleton = new Singleton();
}
}
return singleton;
}
}
问题貌似得以解决,事实并非如此。如果使用以上代码来创建单例对象,还会存在对象不唯一。原因如下:
假如某一瞬间线程 A 和线程 B 都在调用 getInstance() 方法,此时 instance 对象为 null 值,均能通过 “ instance == null ”的判断。由于实现了 synchronize 加锁机制,线程 A 进入 synchronize 锁定的代码中执行实例创建代码,线程 B 处于排队等待状态,必须等待线程 A 执行完毕后才可以进入 synchronize 锁定代码。但当 A 执行完毕时,线程 B 并不知道实例已经创建,将继续创建新的实例,导致产生多个实例,违背了单例模式的设计思想,因此需要进行进一步改进,在 synchronize 锁定代码中在进行一次 "instance == null" 判断,这种方式称为双重检查锁定(Double-Check Locking)。
双重检查锁定:
public class Singleton {
private volatile static Singleton singleton = null; //注意此处加上了volatile关键字
private Singleton(){
System.out.println("构造函数被调用");
}
public static Singleton getInstance(){
if(singleton == null){ //第一重判断
synchronized(Singleton.class){
if(singleton == null){ //第二重判断
singleton = new Singleton(); //创建单例实例
}
}
}
return singleton;
}
}
需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量 instance 之前加修饰符 volatile,被 volatile 修饰的成员变量可以确保多个线程都能正确处理,且该代码只能在 JDK1.5 及以上版本中才能正确执行。由于 volatile 关键字会屏蔽Java 虚拟机所做的一些代码优化 ,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
二、饿汉式单例
图三 饿汉式结构图
单例模式的饿汉式,在定义自身类型的成员变量时就将其实例化,使得在Singleton单例类被系统(姑且这么说)加载时就已经被实例化出一个单例对象,从而一劳永逸地避免了线程安全的问题。代码如下:
public class Singleton{
private static Singleton singleton = new Singleton(); //在定义变量时就将其实例化
private Singleton(){
System.out.println("构造函数被调用");
}
public static Singleton getInstance(){
return singleton;
}
}
当类被加载时,静态变量 instance 会被初始化,此时类的私有构造方法会被调用,单例的唯一实例被创建。在多线程环境下也不会出现线程安全问题,可确保单例对象的唯一性。但是饿汉式单例在类被加载时就创建单例对象并且长驻内存,不管你需不需要它;如果单例类占用的资源比较多,就会降低资源利用率以及程序的运行效率。
饿汉式单例类不能实现延迟加载,不管将来用不用,它始终占据内存;懒汉式单例类线程安全控制繁琐,而且性能受影响。可见,无论是饿汉式还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是肯定的,接下来我就来介绍一种更好的单例模式。
三、一种更好的单例实现方法:静态内部类
静态内部类式在Singleton类内部定义了一个静态的内部类,在该内部类里创建Singleton的单例对象。我们先看代码:
public class Singleton{
private Singleton(){}
private static class HolderClass{ //定义一个静态内部类,该类只会被加载一次
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return HolderClass.instance ;
}
public static void main(String[] args) {
Singleton s1,s2;
s1=Singleton.getInstance();
s2=Singleton.getInstance();
System.out.println(s1==s2);
}
}
编译并运行上述代码,结果为 true : 即创建的单例 s1 和 s2 是同一对象。由于静态单例对象没有作为 Singleton 的成员变量直接实例化,因此类加载时不会实例化 Singleton ,第一次调用 getInstance() 时将加载内部类 HolderClass ,在该内部类中定义了一个 static 类型的变量 instance ,此时会首先初始化这个成员变量,由 Java 虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于 getInstance() 方法没有任何线程锁定,此时其性能不会造成任何影响。
这样,通过静态内部类的方法就实现了lazy loading,很好地将懒汉式和饿汉式结合起来,既实现延迟加载,保证系统性能,也能保证线程安全。但是,它也有缺点,就是静态内部类与编程语言本身的特性相关,很多面向对象语言不支持静态内部类。
然而,对于上述四种方式的单例模式,如果你的Singleton类实现了Serializable序列化接口,那么可能会被序列化生成多个实例,因为readObject()方法一直返回一个新的对象:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Singleton singleton= (Singleton) ois.readObject(); |
这种情况可以通过在Singleton类添加readResolve()方法来解决:
private Object readResolve() {
System.out.println("readResolve()被调用");
return getInstance();
} |
但是这种解决方案虽解决了序列化的问题,但是无法避免被反射。下面还有一种枚举单例,写法简单,还可以避免序列化、反射的问题。
四、枚举单例
上面说到的静态内部类方式不失为一个高级的单例模式实现。但如果开发要求更严格一些,比如你的Singleton类实现了序列化,又或者想避免通过反射来破解单例模式的话,单例模式还可以有另一种形式。那就是枚举单例。枚举类型在JDK1.5被引进。这种方式也是《Effective Java》作者Josh Bloch 提倡的方式,它不仅能避免多线程的问题,而且还能防止反序列化重新创建新的对象、防止被反射攻击。代码如下:
public enum EnumSingleton {
INSTANCE {
@Override
protected void work() {
System.out.println("你好,是我!");
}
};
protected abstract void work(); //单例需要进行操作(也可以不写成抽象方法)
}
在外部,可以通过EnumSingleton.INSTANCE.work()来调用work方法。默认的枚举实例的创建是线程安全的,但是实例内的各种方法则需要程序员来保证线程安全。总的来说,使用枚举单例模式,有三个好处:
1.实例的创建线程安全,确保单例。
2.防止被反射创建多个实例。
3.没有序列化的问题。
单例模式总结:
单例模式作为一种目标明确,结构简单,理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用呢。
1.主要优点
单例模式的主要优点:
(1).单例模式提供了对唯一实例的受控访问。应为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2).由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3).允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获取的指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。
2.主要缺点
单例模式的主要缺点
(1).由于单例模式没有抽象层,因此单例类的扩展有很大的困难。
(2).单例类的职责过重,在一定程度上违背了单一职责。
(3).现在很多面向对象语言(入Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间爱不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景
在以下情况下可以考虑使用单例模式:
(1).系统只需要一个实例对象。例如,系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2).客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。