《设计模式》之单例模式

单例模式的特点

1、只能有一个实例
2、给外部的所有对象提供这一实例。

单例模式有如下几种

1、饿汉式单例模式
2、懒汉式单例模式
3.双重检查式单例模式
4.类级内部类单例模式

下面将进行详细的介绍。

饿汉式单例模式

public class HungrySingle {
    //构造函数私有
    private HungrySingle(){
        //...
    }
    private static HungrySingle instance=new HungrySingle();
    public static HungrySingle getInstance(){
        return instance;
    } 
}

优点:线程安全,调用时反应速度快,在类加载的同时已经创建好了一个静态对象(创建的唯一对象);

缺点:资源利用效率不高,可能该实例并不需要,但也被系统加载了。另外,饿汉式在一些场景下是无法使用的,比如,如果Single实例的创建依赖参数或配置文件,则在getInstance()之前必须调用某个方法来设置这些参数,但在设置之前,可能已经new了Single实例,这种情况下,饿汉式的写法是无法使用的。

懒汉式单例模式

public class LazySingle {
    private LazySingle(){
        //do..
    }
    private static LazySingle lazySingle;
    public static LazySingle getInstance(){
        if(lazySingle==null){
            lazySingle=new LazySingle();
        }
        return lazySingle;
    }

}

优点:延迟加载资源利用率高,只有当调用getInstance函数时,才会实例化对象,但是这导致了第一次加载较慢。
缺点:不是线程安全的;引入多线程时,就必须通过同步(synchronized关键字)来保护getInstance()方法,否则可能会返回LazySingleton的两个不同实例。
例如这种情况:一个线程在判断instance为null后,还没来得及创建新的instance, 另一个线程此时也判断到instance为null,这样两个线程便会创建两个LazySingleton实例。

getInstance()方法前面添加同步之后可以避免上面情况发生,如下

public class LazySingle {
    private LazySingle(){
        //do..
    }
    private static LazySingle lazySingle;
    //对此方法进行同步即可避免线程不安全问题。
    public static synchronized LazySingle getInstance(){
        if(lazySingle==null){
            lazySingle=new LazySingle();
        }
        return lazySingle;
    }

}

优点:线程安全,资源利用率高,不执行getInstance就不会被实例。
缺点:第一次加载时反应不快,多线程使用不必要的同步开销大

上面在方法上加上同步之后,就避免了线程不安全的问题,但是,也带来了效率低下的问题。
为什么说带来了效率低下的问题呢

这是因为每次调用getInstance()方法时,都要同步,而且很多时候的同步是没必要的,这将会极大地拖垮性能(尤其在需要多次调用getInstance方法的地方,当第一次创建了LazySingleton实例后,instance便不再为null,这样后面每次调用getInstance进入方法体后,却便发现自己什么也不用做,而每次调用getInstnce都要同步,需要切换到内核,这样便很浪费资源,每次做很大开销进入方法体,却发现自己什么也不用做)

为了解决上面的效率低下的问题并且保证线程安全,故引入了“双重检查单例模式”

双重检查单例模式

双重检查单例模式,就是我们所熟知的DCL(Double Check Lock);

首先我们来分析没有第二次检查的情况:当instance为null时,两个线程可以并发地进入if语句内部。然后,一个线程进入synchronized块来初始化instance,而另一个线程则被阻断。当第一个线程退出synchronized块时,等待着的线程进入并创建另一个DCLSingle对象。注意:当第二个线程进入 synchronized 块时,它并没有检查 instance 是否非 null。因此我们需要对instance进行第二次非空检查,这也是“双重检查加锁”名称的由来。

public class DCLSingle {
    private DCLSingle(){
        //do..
    }
    private static DCLSingle dclSingle;
    //双重检查实例对象dclSingle是否为空
    public static DCLSingle getInstance(){
        if(dclSingle==null){
            synchronized(DCLSingle.class){
                if(dclSingle==null){
                    dclSingle=new DCLSingle();
                }
            }
        }
        return dclSingle;
    }
}

优点:资源利用率高,不执行getInstance就不会被实例,多线程下效率高。

缺点:第一次加载时反应不快,由于java 内存模型一些原因偶尔会失败,在高并发环境下也有一定的缺陷,虽然发生概率很小。

DCL对instance进行了两次null判断,第一层判断主要是为了避免不必要的同步,第二层的判断则是为了在null的情况下创建实例。

对于DCL的不安全性,我们来看看如下场景:

假设线程A执行到instance = new LazySingleton()这句,这里看起来是一句话,但实际上它并不是一个原子操作,
我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情;
1.给LazySingleton的实例分配内存。
2.初始化LazySingleton()的构造器
3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。

但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Medel,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候instance因为已经在线程A内执行过了第三点,instance已经是非空了,所以线程B直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误很可能会隐藏很久。

注意: DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,但这取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,调整了JMM、具体化了去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.4以及之前的版本,所以人们还在寻找更好的版本。

类级内部类单例模式

上面的单例实现存在小小的缺陷,那么 有没有一种方法,既能够实现延迟加载,又能够
实现线程安全呢?
还真有人想到这样的解决方案了,这个解决方案被称为Lazy initialization holder class 模式,这个模式综合使用了java的类级内部类和多线程缺省同步锁的知识,很巧妙的同时实现了延迟加载和线程安全。

1 相应的基础知识
* (1)什么是类级内部类?
* 简单点说,类级内部类指的是,有static修饰的成员内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
* (2)类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此 可以直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
* (3)类级内部类中,可以定义静态的方法。在静态方法中只能引用外部类中的静态成员方法或变量。
* (4)类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。

采用类级内部类实现的单例模式如下:

public class InnerSingle {
    private InnerSingle(){
        //do..
    }
    private static class SingleHolder{
        public static InnerSingle innerSingle=new InnerSingle();
    }
    public static InnerSingle getInstance(){
        return SingleHolder.innerSingle;
    }
}

这样就满足了延时加载,而不会在类加载器加载其外部类的时候被装载,而且只会被加载一次。因此,资源利用率高。且线程安全;也克服了上面因为同步导致的效率低下的问题。

总结

实际的开发中,在Java中由于会涉及到并发编程,考虑到效率、安全性等问题,我们用的比较多的是:饿汉式单例模式和类级内部类单例模式。而后者资源利用率较高且线程安全,因此更好。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值