深入学习设计模式之---单例模式

在设计模式中,单例模式(singleton)算是应用最普遍的一种设计模式。
顾名思义,单例就是获取对象唯一的实例,它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。在Java语言中,这种行为能带来两大好处:

1、对于频繁使用的对象,可以省略创建对象所花费的时间和消耗,这对于那些重量级对象而言,是非常可观的一笔开销。
2、由于new操作的次数减少,因为对系统内存的使用频率也会降低,这样会减轻GC压力,缩短GC停顿时间。
因此对于系统的关键组件和被频繁使用的对象,如系统中频繁使用的字典值存储等,使用单例模式可以有效的改善系统的性能。



1、简单的实现:
public Class Singleton{
    private Singleton(){
    }
    private static Singleton instance = new Singleton();
    private static Singleton getInstance(){
        return instance;
    }
}

如上例,外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在


2、延迟加载:
上述这个单例实现非常简单,也非常的可靠,它唯一的不足就是无法对类进行延迟加载,例如单例的创建过程很慢,而instance成员变量又是static的,在JVM加载单例类的时候就会被创建,如果这时,这个单例类还在系统中扮演着其他角色,那么在任何使用该单例类中的方法的地方都会初始化这个单例变量,而不管是否被用到。那么怎么办呢?
可以看一下下面的方案:

public Class LazySingleton{
    private LazySingleton(){
    }
    private static LazySingleton instance = null;
    private static synchronized LazySingleton getInstance(){
        if(instance == null ){
            instance = new LazySingleton();
        }
        return instance;
    }
}

首先对于静态成员变量instance初始值赋予null,确保系统启动的时候没有额外的负荷。其次在调用getInstance()方法时,会先判断当前单例是否已经存在,若不存在,再创建单例,避免了重复创建,但在多线程中必须要用synchronized关键字修饰,避免线程A进入创建过程还未创建完毕之前线程B也通过了非空判断导致重复创建。


3、同步性能
上述代码看起来已经完美的实现了单例,有了同步锁,一个线程必须等待另外一个线程创建完成后才能使用这个方法,这就保证了单例的唯一性。但是大家都知道,被synchronized修饰的同步块要比一般代码慢上很多倍,如果存在很多次getInstance()的调用,那性能问题就不得不考虑了!那么为了引入延迟加载而使用同步关键字反而降低了系统性能,是否有些得不偿失呢?

让我们来分析一下,究竟是整个方法都必须加锁,还是仅仅其中某一句加锁就足够了?我们为什么要加锁呢?分析一下出现延迟加载的那种情形的原因。原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。于是,我们开始修改代码:

public Class LazySingleton{
    private LazySingleton(){
    }
    private static LazySingleton instance = null;
    private static LazySingleton getInstance(){
        synchronized (LazySingleton.class) { 
            if(instance == null ){
                instance = new LazySingleton();
            }
        }
        return instance;
    }
}

首先去掉getInstance()的同步操作,然后把同步锁加载if语句上。但是这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要同步,性能问题还是存在。如果……如果我们事先判断一下是不是为null再去同步呢?
public Class LazySingleton{
    private LazySingleton(){
    }
    private static LazySingleton instance = null;
    private static LazySingleton getInstance(){
        if(instance == null){
            synchronized (LazySingleton.class) { //语句1
                if(instance == null ){    //语句2
                    instance = new LazySingleton();//语句3
                }
            }
        }
        return instance;
    }
}

还有问题吗?首先判断instance是不是为null,如果为null,加锁初始化;如果不为null,直接返回instance。
到此为止,一切都很完美。我们用一种很取巧的方式实现了单例模式。



4、Double-Checked Locking
这其实是Double_Checked Locking设计实现的单例模式
到目前为止,看似已经完美的解决了问题,性能和实现共存,但真的是这样吗?
让我们来分析一下:
让我们直接看语句3,JIT产生的汇编代码所做的事情并不是直接生成一个LazySingleton对象,然后将地址赋予instance,相反的,它的执行顺序如下:
1.先申请一块内存
2.将这块内存地址赋予instance
3.在instance所指的地址上构建对象

试想一下:如果线程调度发生在 instance 已经被赋予一个内存地址,而 Singleton 的构造函数还没有被调用的微妙时刻,那么另一个进入此函数的线程会发觉 instance 已经不为 null ,从而放心大胆的将 instance 返回并使用之。但是这个可怜的线程并不知道此时 instance 还没有被初始化呢!
由于Java的memory model允许out-of-order write,现在的症结就在于:首先,构造一个对象不是原子操作,而是可以被打断的;第二,更重要的,Java 允许在初始化之前就把对象的地址写回,这就是所谓 out-of-order 。


5、实现方案
扯了这么多,怎么就没有个完美的实现方案?
在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了

public Class LazySingleton{
    private LazySingleton(){
    }
    private volatile static LazySingleton instance = null;
    private static LazySingleton getInstance(){
        if(instance == null){
            synchronized (LazySingleton.class) { //语句1
                if(instance == null ){    //语句2
                    instance = new LazySingleton();//语句3
                }
            }
        }
        return instance;
    }
}

volatile关键字保证了内存可见性,最新的值可以立即被所有线程可见

其实还有另外一种实现方案,就是采取内部类

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


在这个实现中,单例模式使用了内部类来维护单例的实例,当Singleton被加载时,内部类不会被初始化,可以确保当Singleton被加载到虚拟机时,不会初始化单例类,而当getInstance()方法被调用时,才会加载Singleton,从而初始化instance,并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。

通常情况下,5里面的两种方式已经可以确保系统中只存在唯一的实例了,但仍然也有例外情况,可能导致系统存在多个实例,比如通过反射机制,调用单例类里面的私有构造函数,就有可能生成多个单例,但是这种情况的特殊性,在此就不做讨论了,否则实现完美单例模式就真的彻底某盖了!

猜你喜欢

转载自swolf119.iteye.com/blog/2209176