首页 > 代码库 > 常见的设计模式:单例模式

常见的设计模式:单例模式

首先要明确一个概念,什么是设计模式?设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式往往代表要解决某方面问题的最佳实现,通常被有经验的面向对象的软件开发人员所采用。 

那么什么是单例模式呢?单例模式的定义是:一个类,在全局当中,有且仅含有一个实例,并且一般是由自身去进行实例化,再向外提供一个统一的获取方法,外界只能获取到该实例,但无法对实例进行修改,仅能对其进行读取或者应用,外界在调用这个类的时候,都是调用了同一个实例,最常用的场景就是多程序读取一个配置文件时,建议配置文件封装成对象,并且使用单例模式,会方便操作其中的数据,又要保证多个程序读到的是同一个配置文件对象,就需要该配置文件对象在内存中是唯一的。简单说来,单例模式(也叫单件模式)的作用就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例都最多只存在一个(当然也可以不存在)。

基于以上描述,那么在设计单例模式的情况下,我们应该要让设计的当前类满足一下条件,才能被称为单例模式:

1.其他类和程序无法实例化当前类,所以可以知道当前类的构造函数一定是用private去进行修饰的(不可不写构造函数,因为默认的构造函数是public的)

2.当前类要可以在类中实例化唯一一个自身对象,所以这个实例可以是静态static的(根据实例化的时机不同,可以分为饿汉模式和懒汉模式,之后有说)

3.当前类要可以向外界提供一个统一的接口,提供实例,向外界提供一个getInstance方法,并且返回一个自身的唯一实例,因为其他的类无法实例化该类,所以该方法也必须是静态Static的(要么其他的类是无法获取到该类的实例的)。

基于上述思路,可以完成如下单例模式的代码:

package wellhold.bjtu.singleton;

//饿汉模式
public class Single {

    private static Single single=new Single();
    
    private Single()
    {

    }
    public static Single getInstance()
    {
        return single;
    }

}

在代码和前文当中,提到了两个名词,那就是饿汉模式和懒汉模式,所谓的饿汉模式,就是在程序启动的时候,类在加载的时候,该单例类的实例就已经是存在的模式,即从代码当中看,在Single类在被加载的时候,就已经实例化一个实例,叫single,并且静态的存储在了内存当中,等待其他的类或程序块调用,这种模式就是饿汉模式。饿汉模式毋庸置疑是线程安全的,因为该实例是在类加载的时候就已经存在内存当中,并且其他的类仅能调用它,不能修改它。

而相对来说,懒汉模式则与饿汉模式相反,懒汉模式下,单例类在被加载的时候,实例还不存在,仅在其他对象在调用单例类提供的获取实例方式的时候,单例类才去创建这个唯一的实例(如果之前已经创建过,则不进行创建),之后再返回实例,这种模式,叫做懒汉模式。可以实现代码如下:

package wellhold.bjtu.singleton;

//懒汉模式
public class Single {

    private static Single single;
    
    private Single()
    {

    }
    public static Single getInstance()
    {
        if(single==null)
            single=new Single();
        return single;
    }

}

可以从代码当中看出,懒汉模式这种情况下,是线程不安全的,具体可以见图(图来自:http://www.cnblogs.com/ysw-go/p/5386161.html)

技术分享

两个线程,线程一和线程二同时调用了getInstance方法,当线程1执行了if判断,single为空,还没来得及执行single =new Single()创建对象,这个时候线程2就来了,它也进行if判断,single依然为空,则创建Single对象,此时,两个线程就会创建两个对象,违背我们单例模式的初衷。那么要解决这个线程安全的问题,可以使用以下三种方法:

1.加同步锁:

package wellhold.bjtu.singleton;

//懒汉模式-线程安全模式一
public class Single {

    private static Single single;
    
    private Single()
    {

    }
    public synchronized static Single getInstance()
    {
        if(single==null)
            single=new Single();
        return single;
    }

}

但这种情况下,同步是需要开销的,而我们只需要在创建实例化的时候同步,所以又衍生出了第二种模式。

2.双重检查锁定

package wellhold.bjtu.singleton;

//懒汉模式-线程安全模式二
public class Single {

    private static Single single;
    
    private Single()
    {

    }
    public static Single getInstance()
    {
        if(single==null)
            synchronized(Single.class)
            {
                if(single==null)
                    single=new Single();
            }
        return single;
    }

}

这样一种设计可以保证只产生一个实例,并且只会在初始化的时候加同步锁,看似精妙绝伦,但却会引发另一个问题,这个问题由指令重排序引起。

指令重排序是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。

例如 instance = new Singleton() 可分解为如下伪代码:

 
  1. memory = allocate();   //1:分配对象的内存空间  
  2. ctorInstance(memory);  //2:初始化对象  
  3. instance = memory;     //3:设置instance指向刚分配的内存地址  

但是经过重排序后如下:

 
  1. memory = allocate();   //1:分配对象的内存空间  
  2. instance = memory;     //3:设置instance指向刚分配的内存地址                       
  3.  //注意,此时对象还没有被初始化!  
  4. ctorInstance(memory);  //2:初始化对象  

将第2步和第3步调换顺序,在单线程情况下不会影响程序执行的结果,但是在多线程情况下就不一样了。线程A执行了instance = memory(这对另一个线程B来说是可见的),此时线程B执行外层 if (instance == null),发现instance不为空,随即返回,但是得到的却是未被完全初始化的实例,在使用的时候必定会有风险,这正是双重检查锁定的问题所在! 

这个问题在 J2SE 5.0 中已经被修复,可以使用 volatile 关键字来保证多线程下的单例

 

//懒汉模式-线程安全模式二完整版
public class Single {

    private static volatile Single single;
    
    private Single()
    {

    }
    public static Single getInstance()
    {
        if(single==null)
            synchronized(Single.class)
            {
                if(single==null)
                    single=new Single();
            }
        return single;
    }

}

3.静态内部类方式实现懒汉模式的单例模式

package wellhold.bjtu.singleton;

//懒汉模式-线程安全模式三
public class Single {

    private static class LazyHolder{
        
        private static final Single single=new Single();
    }
    
    private Single()
    {

    }
    public static Single getInstance()
    {
        return LazyHolder.single;
    }
}

这种模式是懒汉模式当中三中模式当中的最佳,即保证了实例化延迟,也保证了不浪费性能。

常见的设计模式:单例模式