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

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

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


[b][size=medium]1、简单的实现:[/size][/b]

public Class Singleton{
private Singleton(){
}
private static Singleton instance = new Singleton();
private static Singleton getInstance(){
return instance;
}
}

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


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

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

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


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

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

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;
}
}

[size=small]首先去掉getInstance()的同步操作,然后把同步锁加载if语句上。但是这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要同步,性能问题还是存在。如果……如果我们事先判断一下是不是为null再去同步呢?[/size]

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;
}
}

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


[b][size=medium]4、Double-Checked Locking[/size][/b]
[size=small]这其实是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 。[/size]

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

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;
}
}

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

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

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

private Singleton(){
}
}


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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值