单列模式详解-java实现

本篇文章摘自数据《漫谈设计模式》,看到了觉得好,就像分享给大家,是我辛辛苦苦一个字一个字敲上去的,大家喜欢就赞一个,有不好的希望大家留下评论,我一定改正。

一:最简单的单列

为了限制类的对象被随意的创建,需要保证该类的构造方法是私有的,这样外部类就无法创建该类型的对象了;另外,为了方面给客户对象提供此单列对象的使用,我们为它提供一个全局访问点,代码如下所示

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        return instance;
    }
}
代码注解:

Singleton类只有一个构造方法,它是被Private修饰的,客户对象无法创建该类实例。

我们为此单列实现的全局访问点是public static Singleton getInstance()方法。

注意:instance变量是私有的,外界无法访问。

读者还可以定义instance变量是public,这样把属性直接暴露给其他对象,就没有必要实现public static Singleton getInstance()方法,但是可读性没有方法来的直接,而且把实例对象的名字直接暴露给客户程序,会增加代码的耦合度,如果改变此变量名称,就会引起客户类的改变。

还有一点,如果该实例需要比较复杂的初始化过程时,把这个程序因该写在static{...}静态代码块中。

此实现是线程安全的,当多个线程同时去访问该类的getInstance()方法时,不会初始化多个不同的对象,这是因为,JVM(java virtual machine)在加载此类时,对于static属性的初始化只能由一个线程执行且仅一次。

由于此单列提供了静态的公有方法,那么客户端使用单列模式的代码也就非常简单了,如下所示。

Singleton singleton = Singleton.getInstance();

 

二:单列进阶

如果处于性能方面的考虑,我们希望延迟实例化单列对象(Static属性在加载类时候就会被初始化),只有在第一次使用该类的实例时才去实列化,我们因该怎么办呢?

1:延迟创建

这个其实并不难做到,我们把单列的实列化过程移至getInstance()方法,而不再加载类的时候预先创建。当访问此方法时,首先判断该实列是不是已经被实列化过了,如果已经被初始化了,则直接返回这个对象的引用;否则,创建这个实列并初始化,最后返回这个对象的引用。代码片段如下所示:

public class UnThreadSafeSingleton {

    public static UnThreadSafeSingleton getSingleton()
    {
        if(null==instance)
        {
            instance = new UnThreadSafeSingleton();
        }
        return instance;
    }
}

我们使用这句if(null==instance)判断是否实列化完成了。此方法不是线程安全的,接下来我们将会讨论。

2:线程安全

上节我们创建了可延迟初始化的单列,然而不幸的是,在高并发的环境中,getInstance()方法返回了多个指向不同的该类实列,究竟是什么原因呢?我们针对此方法,给出两个线程并发访问getInstance()方法时的一种情况,如下所示。

         Thread1                                                           Thread2

if(null==instance)                                                 if(nulll==instance)

instance = new UnThreadSafeSingelton();           instance = new UnThreadSafeSingelton();

return instance;                                                     return instance;

如果这两个线程按照上述步骤执行,不难返现,在时刻1和2,由于还没有创建单列对象,Thread1和Thread2都会进入创建单列实列的代码块分别创建实列。在时刻3,Thread1创建了一个实列对象,但是Thread2此时已经无法知道,于是继续创建一个新的实列对象,导致这两个线程持有的实列并非为同一个。

更为糟糕的是,在没有自动内存回收机制的语言平台上运行这样的单列模式,例如C++编写此模式,因为我们认为创建了一个单列实列,忽略了其它线程所产生的对象,不会手动去回收它们,从而引起了内存泄露。

为了解决这个问题,我们给此方法添加synchronized关键字,代码如下。

public class ThreadSafeSingelton{

public static synchronized ThreadSafeSingelton getInstance(){

            if(null==instance){

                    instance = new ThreadSafeSingelton();

                }

                return instance;

        }

}

这样,再多的线程访问都只会实列化一个单列对象。

3:Double-Check Locking  双重检验锁

上述途径虽然实现了多线程的安全访问,但是在多线程高并发访问的情况下,给此方法加上synchronized关键字会使得性能大不如从前。

我们仔细分析一下不难发现,使用synchronized关键字对整个getInstance()方法进行同步是没有必要的,我们只需保证实例化这个对象的那段逻辑被一个线程执行就可以了,而返回引用的那段代码是没有必要同步的。按照这个想法,代码片段大致如下所示。

public class DoubleCheckSingleton{
    
    private volatile static DoubleCheckSingleton instance = null;
    
    public static DoubleCheckSingleton getInstance(){
        if(null==instance){//检验是否被创建
            synchronized (DoubleCheckSingleton.class){
                //创建同步锁
                if(null==instance){//再次检验是否被创建
                    instance = new DoubleCheckSingleton();
                    
                }
            }
            return instance;
        }
    }
}

代码注解:

在getInstance()方法里面,首先判断此实列是否已经被创建了,如果还没有创建,首先使用synchronized同步实列化代码块。在同步代码块里面,还需要再次检验是否已经创建了此类的实列,这是因为:如果没有第二次检查,这时候有两个线程ThreadA和ThreadB同时进入该方法,他们都检测到了instance为null,不管哪一个线程先占据同步锁,并创建实列对象,都不会阻止另外一个线程继续进入实例化代码块重新创建实列对象,这样,同样会生成两个实列对象。所以,我们在同步代码块里,要进行第二次判断,判断该对象是否已经被创建。

属性instance是被volatile修饰的,因为volatile具有synchronized的可见性特点,也就是说线程能够自动发现volatile变量的最新值。这样,如果instance实列化成功,其它线程便能立即发现。

注意:此程序只有在java5及以上版本才能正常运行,在以前版本不能保证其正常运行,这是由于java平台的内存模式容许out-of-order writes引起的,假定有两个线程,Thread1和Thread2,它们执行以下步奏:

1、Thread1发现instance没有被实列化,它获得锁,并且实列化此对象,JVM容许在没有完全实列化完成时instance变量就指向此实列,因为这些步奏可以是out-of-order writes的,此时instance==null为false,之前的版本即使用volatile关键字修饰也无效。

2、在初始化完成之前,Thread2进入此方法,发现instance已经不为null了,Thread2便认为该实列初始化完成了,使用这个为完全初始化的实列对象,则很可能引起系统的崩溃。

4:Initialization on demand holder

要使用线程安全的延迟的单列初始化,我们还有一种方法,称为Initialization on demand holder模式,代码如下所示。

public class LazyLoadedSingleton{
    private LazyLoadedSingleton(){
        
    }
    private static class LazyHolder{//holds the singleton class
        private  static final LazyLoadedSingleton singletonInstance = new LazyLoadedSingleton();
    }
    public static LazyLoadedSingleton getInstance(){
        return LazyHolder.singletonInstance;
    }
}

当JVM加载LazyLoadedSingleton类时,由于该类没有static属性,所以加载完成后便即可返回。只有第一次调用getInstance()方法时,JVM才会加载LazyHolder类,由于它包含一个static属性singletonInstance,所以会首先初始化这个变量,根据前面的介绍,我们知道此过程并不会出现并发问题(JLS保证),这样即实现了一个既保证了线程安全又支持延迟加载的单列模式。

 

5:Singleton的序列化

如果单列类实现了Serializable接口,这时候要特别注意,因为在默认情况下,每次反序列化总会创建一个新的实列对象,这样一个系统会出现多个对象供使用。我们因该怎么办呢?

熟悉java序列化的读者可能知道,我们需要在readResolve()方法里做文章,此方法在反序列化完成之前被执行,我们在此方法里替换掉反序列化出来的那个新的实列,让其指向内存中的那个单列对象即可,代码实现如下。

public class SerialibleSingleton implements Serializable{
    private static final long serialVersionUID = -6099617126325157499L;
    static SerialibleSingleton singleton = new SerialibleSingleton();
    
    private SerialibleSingleton(){
        
    }
    
    //this method is called immediately after an object of this class is deserialized.
    //this method return the singleton instance
    private Object readResolve(){
        return singleton;
    }
}

方法readResolve()直接返回singleton单列,这样,我们在内存中始终保持了一个唯一的单列对象。

注:在分布式环境中,我们可能还需要考虑如何保证在整个应用(可能分布在不同JVM)只有一个实列。

转载于:https://my.oschina.net/dreamerliujack/blog/818859

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值