经常使用设计模式之单例模式

单例对象(Singleton)是一种经常使用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只
有一个实例存在。这样的模式有几个好处:
一、某些类建立比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
二、省去了new操做符,下降了系统内存的使用频率,减轻GC压力。
三、有些类如交易所的核心交易引擎,控制着交易流程,若是该类能够建立多个的话,系统完
全乱了。设计模式

例子:
//简单的单例类 饿汉模式
public class Singleton { 安全

  /* 持有私有静态实例,防止被引用*/
  private static Singleton instance = new Singleton(); 多线程

  /* 私有构造方法,防止被实例化 */
  private Singleton() {
  } 函数

  /* 静态工程方法,返回Singleton实例 */
  public static Singleton getInstance() {
    return instance;
  } 性能

  /* 若是该对象被用于序列化,能够保证对象在序列化先后保持一致 */
  private Object readResolve() {
    return instance;
  }
} 测试

这个类是能够实现单例模式的,可是存在很多问题,好比在类中无论用户是否要使用该类的对象,就先建立
好了一个实例放在内存中。优化

//简单的单例类 懒汉模式
public class Singleton { 线程

  /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
  private static Singleton instance = null; 设计

  /* 私有构造方法,防止被实例化 */
  private Singleton() {
  } 对象

  /* 静态工程方法,建立实例 */
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }

  /* 若是该对象被用于序列化,能够保证对象在序列化先后保持一致 */
  private Object readResolve() {
    return instance;
  }
}

这个类能够知足基本要求,可是,像这样毫无线程安全保护的类,若是咱们把它放入多线程的环境下,确定
就会出现问题了,如何解决?咱们首先会想到对getInstance方法加synchronized关键字,以下:

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

可是,synchronized做为修饰符在方法上使用,在性能上会有所降低,由于每次调用getInstance(),都要对
对象上锁,事实上,只有在第一次建立对象的时候须要加锁,以后就不须要了,因此,这个地方须要改进。
咱们改为下面这个:
public static Singleton getInstance() {
  if (instance == null) {
    synchronized (instance) {
      if (instance == null) {
        instance = new Singleton();
      }
    }
  }
  return instance;
}

彷佛解决了以前提到的问题,将synchronized关键字加在了方法内部,也就是说当调用的时候是不须要加锁
的,只有在instance为null,并建立对象的时候才须要加锁,性能有必定的提高。可是,这样的状况,仍是
有可能有问题的。
看下面的状况:在Java指令中建立对象和赋值操做是分开进行的,也就是说instance = new Singleton();
语句并不是是一个原子操做,在 JVM 中这句代码大概作了下面 3 件事情:
  1给 new的对象 分配内存
  2调用 Singleton 的构造函数来初始化成员变量
  3将引用instance指向分配的内存空间(执行完这步 instance 就为非 null 了)
可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在 3 执行完毕、2 未执行以前,另一个线程B抢夺到了CPU的执行权,这时instance已是非null了(但却没有初始化),因此线程B会直接返回 instance,而后使用,结果就会出现问题了(由于对象尚未初始化)。

还有另一种解决方案:使用内部类来维护单例的实现,JVM内部的机制可以保证当一个类被加载的时候,这
个类的加载过程是线程互斥的(就是加载完毕后别的线程才能使用)。这样当咱们第一次调用getInstance的
时候,JVM可以帮咱们保证instance只被建立一次,而且会保证把赋值给instance的内存初始化完毕,这样
咱们就不用担忧上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能
问题。例如:

public class Singleton {

  /* 私有构造方法,防止被实例化 */
  private Singleton() {
  }

  /* 此处使用一个内部类来维护单例 */
  private static class SingletonFactory {
    private static Singleton instance = new Singleton();
  }

  /* 获取实例 */
  public static Singleton getInstance() {
    return SingletonFactory.instance;
  }

  /* 若是该对象被用于序列化,能够保证对象在序列化先后保持一致 */
  private Object readResolve() {
    return getInstance();
  }
}

可是若是在构造函数中抛出异常,实例将永远得不到建立,也会出错。因此说,十分完美的东西是没有的,
咱们只能根据实际状况,选择最适合本身应用场景的实现方法。

同时,咱们还可使用反射去建立这个类的对象,即便它的构造器是私有的,咱们也是能够调用到的。那
么这个时候咱们就须要再次修改代码去访问别人反射调用构造器。
例子:
//在构造器中控制一下,构造器只容许调用一次,以后在调用就抛出异常
public class Singleton {
  private volatile static boolean flag;
  /* 私有构造方法,防止被实例化 */
  private Singleton() {
    if(!flag){
      flag = false;
    }else{
      throw new RuntimeException("不能屡次建立单例对象");
    }
  }

  /* 此处使用一个内部类来维护单例 */
  private static class SingletonFactory {
    private static Singleton instance = new Singleton();
  }

  /* 获取实例 */
  public static Singleton getInstance() {
    return SingletonFactory.instance;
  }

  /* 若是该对象被用于序列化,能够保证对象在序列化先后保持一致 */
  private Object readResolve() {
    return getInstance();
  }
}

反射的问题处理完了以后,这里还有一个问题,就是若是把单例对象进行序列化而后再反序列化,那么内存
中就会出现俩个同样的单例对象,只是内存地址不一样。这种状况咱们可使用readResolve方法来防止。
private Object readResolve(){.....}
ObjectInputStream 会检查对象的class是否认义了readResolve方法。若是定义了,将由readResolve方法
指定返回的对象。返回对象的类型必定要是兼容的,不然会抛出ClassCastException 。
例子:
public abstract class Singleton8 implements Serializable{

  private static final long serialVersionUID = 7863921642928237696L;

  /* 此处使用一个内部类来维护单例 */
  private static class SingletonFactory {
    private static Singleton8 instance = new Singleton8(){};
  }

  //测试方式,把单例对象序列化后再反序列化从而得到一个新的对象 就至关于复制了一个单例对象
  public Singleton8 copy() throws Exception{
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(os);
    oos.writeObject(Singleton8.getInstance());
    InputStream is = new ByteArrayInputStream(os.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(is);
    Singleton8 obj = (Singleton8) ois.readObject();
    return obj;
  }

  /* 获取实例 */
  public static Singleton8 getInstance() {
    return SingletonFactory.instance;
  }

  /* 若是该对象被用于序列化,能够保证对象在序列化先后保持一致 */
  /* 把这个方法注释前和注释后来运行测试代码观察结果 */
  private Object readResolve() {
    return getInstance();
  }
}

实现和完善单例模式的方法有不少,笔者就不一一列举了。

相关文章
相关标签/搜索