天天看点

设计模式之——单例设计

转载请说明来自:http://blog.csdn.net/super_kingking/article/details/51277238

前言:我们在开发过程中都会用到单例设计模式,但是为什么我们要用单例呢?单例设计模式的有点和缺点以及单利设计模式的几种形式?

我们为什么要用单例呢?因为单例可以减轻加载的负担,缩短加载的时间,提高加载的效率。

单例适用的方面:

  1. 控制资源的使用,通过线程同步来控制资源的并发访问。
  2. 控制实例的产生,以达到节约资源的目的。
  3. 通过数据共享,在不建立直接关联的条件下,让多个不相关的线程之间通信。

首先我们先来看一下单例设计模式的写法:

饿汉式设计模式

public class Singleton{
    //在自己内部定义自己的一个实例,只供内部调用
    private static final Singleton instance = new Singleton();
    private Singleton(){

    }
    //这里提供了一个供外部访问本class的静态方法,可以直接访问
    public static Singleton getInstance(){
        return instance;
    }
}
           

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。但是如果我没有调用getInstance方法,instance实例已经存在,这样造成不必要的资源消耗,肯定希望在需要的时候才去创建instance对象。

懒汉式

public class SingleTon {
    //instance需要用static修饰,在static方法中,引用的变量类型必须也是static类型的

    private static SingleTon instance;
    //构造函数要private修饰,私有化,只能在SingleTon类中创建实例化,避免在外部类中进行实例化。
    private SingleTon() {
    }

    public static SingleTon getInstance() {
        if (instance== null) {
            instance= new SingleTon();
        }
        return instance;
    }
}
           

Singleton的唯一实例只能通过getInstance()方法访问。这样的代码如果在多个线程当中是不安全的,所以我们需要进行改进。

**getInstance方法上加同步**
public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {
    }

    public static synchronized  SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance ;
    }
}
           

这样的已经很安全,有效的解决了可能会在多个线程中创建出多个instance对象,但是现在又存在一个新的问题,在方法上添加synchronized同步,每次在调用getInstance的时候都会受到同步锁的影响,这样是非常影响效率的。接下来我们再进行改进。

//我们将synchronized加到方法体中
public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {
    }

    public static SingleTon getInstance() {
    //但是在这里添加和上面的效果其实是一样的,我们还需要进行下一句的优化
        synchronized(SingleTon.class)  {

            if (instance== null) {
                instance= new SingleTon();
            }
            return instance;
        }
      }
    }
           

DCL (Double Check Lock)双重检查锁定

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

这种双重锁定的模式,代码再进入第6行的时候,首先会判断instance是否为空,避免了不必要的同步,第2个判断则是为了在null的情况下创建实例,这种方式叫做双重锁定单例。

下面分析一下DCL的步骤,当线程1执行了 instance = new SingleTon()语句,这句话看起来是一句话,但是实际上它做了3件事:

  • (1)首先给instance实例分配内存空间(空的内存)
  • (2)调用new SingleTon()构造函数,初始化成员字段
  • (3)将instance引用指向内存分配的空间地址,此时的instance已经不在是null的了。

JVM编译器的指令重排序,并不是随着代码的一条条顺序执行的,所以会出现1-2-3的执行顺序,也会出现1-3-2的执行顺序,如果是1-3-2的执行顺序,3执行完毕,2未执行前,切换到线程2中去,instance此时已经指向了内存分配的地址,已经不为null,但是返回的是个没有初始化完成的instance对象,这时在线程2中使用就会出错。

在JDK1.5之后修改了这个bug,sun公司提供了volatile关键字:1.可见性:线程在每次使用变量的时候,都会从主内存中读取变量修改后的值。2.阻止指令重排序

将instance修改定义为private volatile static SingleTon instance = null,

public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
          if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}
           

当线程1执行 instance = new SingleTon()的时候,JVM的执行顺序必须是

- (1)首先给instance实例分配内存

- (2)调用new SingleTon()构造函数,初始化成员字段

- (3)将instance引用指向内存分配的空间地址。

在线程2看来,instance对象要么指向一个初始化完整的instance对象,要么是null,不会出现中间值。

总结: volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值

静态内部类单例模式:

DCL (Double Check Lock)虽然在一定程度上是解决了资源消耗,多余的同步,线程安全等问题,但是在高并发环境或者jdk1.6版本下使用下也会有一定的缺陷,java内存模型的原因偶尔也是会失败的,但是概率很小。如果使用静态内部类单例模式则安全,高效,唯一:

public class SingleTon {
    private SingleTon (){

    }
    public static Singleton getInstance(){
        return SingleTonHolder.singleTon ;
    }
    private static class SingleTonHolder{
        private static final SingleTon singleTon  = new SingleTon();
    }
}
           

第一此加载Singleton类时并不会初始化instance,只有在第一次调用getInstance()方法时虚拟机才会加载SingleTonHolder类,然后对instance进行初始化,该形式是利用了ClassLoader的加载机制来实现懒加载,这种方式不仅能够保证线程的安全,而且能够保证单例对象的唯一性,同时还延迟了单例的实例化,所以更推荐这种方式。

缺点:双重检查机制和静态内部类虽然很好,但是依然无法防止利用反射来重复构建对象。

利用反射构建单例对象:

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));//false
//得到的结果是false,说明这里是2个不同的对象。
           

那怎么才能阻止反射的构建方式呢?:枚举实现单例设计模式

public enum SingletonEnum {
    INSTANCE;
}
           

使用枚举单例不但能保证防止反射构造对象,而且也能保证线程安全,但是其并不是使用线程安全,单例对象INSTANCE是在枚举类被加载的时候进行初始化的。