疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

Java创建单例的5中常见方式


 

   

  //创建单例综合来说有五种方式

  public class JustSingleTon {

  public static void main(String[] args) {

  System.out.println("单例应该怎么用?");

  }

  }

  // 第一种:饿汉式

  class SingleTon_1 {

  private SingleTon_1() {

  }

  private final static SingleTon_1 instance = new SingleTon_1();

  public final static SingleTon_1 getInstance() {

  return instance;

  }

  }

  // 第二种:懒汉式(线程安全的)

  class SingleTon_2 {

  private SingleTon_2() {

  }

  private static SingleTon_2 instance = null;

  public final static synchronized SingleTon_2 getInstance() {

  if (instance == null) {

  instance = new SingleTon_2();

  }

  return instance;

  }

  }

  // 第三种:双重校验锁(线程安全的-效率更高)

  class SingleTon_3 {

  private SingleTon_3() {

  }

  private volatile static SingleTon_3 instance = null;

  public final static SingleTon_3 getInstance() {

  if (instance == null) {

  synchronized (SingleTon_3.class) {

  if (instance == null) {

  instance = new SingleTon_3();

  }

  }

  }

  return instance;

  }

  }

  // 第四种:内部类方式

  class SingleTon_4 {

  private SingleTon_4() {

  }

  private final static class innerSingleTon {

  private static final SingleTon_4 instance = new SingleTon_4();

  }

  public final static SingleTon_4 getInstance() {

  return innerSingleTon.instance;

  }

  }

  // 第五种:枚举

  enum SingleTon_5 {

  INSTANCE;

  }

  /*

  * 创建单例的五种方式:懒汉、饿汉、双重校验锁、内部类、枚举

  *

  * PS:上述的五种方式的单例类都应该是public的,但因只能有一个public的限制而未加(不想看到IDE满屏幕飘红),实际使用应加上public关键字。

  *

  * 提出的问题是:上面的单例模式真的能保证都只存在一个实例?

  *

  * 答案是否定的,突破单例的方式有以下几种:

  *

  * 1-多线程并发

  * 2-序列化

  * 3-反射

  * 4-类加载器

  *

  * 【问题1】 对于问题1,上面的五种都做了最基础的优化以避免出现线程安全问题。没有太多研究价值。

  *

  * 【问题2】 对于问题2,除了方式5,其他的都存在这个问题(通过实现java.io.Serializable接口来克隆出更多的实例)

  * 解决的方式是重写Serializable中的

  * readResolve()方法,将任何试图通过序列化来获得更多类的序列化流都替换为已有的而且是首次创建的那个实例。

  *

  * 防御示例:

  *

  * public class SingleTon implements java.io.Serializable{

  * private static final

  * long serialVersionUID = 1L; private SingleTon(){}

  *

  * public final static Single INSTANCE=new Singleton();

  *

  * private Object readResolve(){

  * return INSTANCE;

  * }

  * 正如所看到的那样,在实现了序列化的单例中添加一个readResolve()方法就可以阻止通过序列化来破坏单例模式。

  *

  * 该方法的API描述:(writeReplace()和readResolve()方法是一对读写方法)

  *

  * 在序列化流不列出给定类作为将被反序列化对象的超类的情况下,readObjectNoData 方法负责初始化特定类的对象状态。

  * 这在接收方使用的反序列化实例类的版本不同于发送方,并且接收者版本扩展的类不是发送者版本扩展的类时发生。

  * 在序列化流已经被篡改时也将发生;因此,不管源流是“敌意的”还是不完整的,readObjectNoData 方法都可以用来正确地初始化反序列化的对象。

  * 将对象写入流时需要指定要使用的替代对象的可序列化类,应使用准确的签名来实现此特殊方法:

  *

  * ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

  *

  * 此 writeReplace 方法将由序列化调用,前提是如果此方法存在,而且它可以通过被序列化对象的类中定义的一个方法访问。 因此,该方法可以拥有私有

  * (private)、受保护的 (protected) 和包私有 (package-private) 访问。 子类对此方法的访问遵循 java 访问规则。

  *

  * 在从流中读取类的一个实例时需要指定替代的类应使用的准确签名来实现此特殊方法。

  *

  * ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

  *

  * 此readResolve 方法遵循与 writeReplace 相同的调用规则和访问规则。

  *

  * 【问题3】 对于问题3,也就是反射,通过反射的getDeclaredConstructors()可以获得指定类的所有构造器,突破所有的访问权限限制。

  * 只要有了构造器就可以new出任意多个对象了,只要你愿意。所以很容易就突破了单例模式。需要注意的是:上述的方式5,也就是枚举方式

  * 并不会受到影响,通过枚举获得的仍然只有一个实例;原因在于所有的枚举类都继承自java.lang.Enum下的abstract枚举基类,该类规定了

  * 枚举只能有创建枚举类初始化时的有限个的实例,不会因任何方式被改变。更多的信息可以查看下相关API。

  *

  * PS:枚举是从JDK5.0开始新增的特性。

  *

  * 那么对于其他几种方式该如何阻止通过反射破坏单例模式呢?

  * 构造器。所有的实例对象都是通过构造器来初始化的,那么就可以在构造器中设置障碍,使得构造器只能被用来初始化一个实例,以后的调用直接返回

  * 首次创建的实例,更灵活的,我们也可以选择抛出异常来警告试图破坏单例模式的行为。

  *

  * 防御示例:

  *

  * .......

  * private boolean flag=true;

  * private(){

  * if(flag){

  * flag=!flag;

  * }else{

  * //我们可以选择抛出异常用以警告

  * //throw RuntimeException("不能创建更多的实例!");

  * //或者直接返回首次创建的实例

  * return instance;

  * ......

  *

  * 【问题4】对于问题4,上述5种方式都存在。根源在于类加载器的加载机制,每一个类只能在同一个加载器下加载一次,如果存在多个加载器,那么就等于

  * 可以有相应多个其他的该类的实例(在每一个类加载器中仍然只有一个实例),这种方式显然也破坏了单例模式。

  * 例如servlet容器对其他多个servlet使用不同的加载器进行加载的情况。

  * 当然,我们也可以手动来实现多个不同类加载器对同一个类的加载,这种方式更像黑客的强行破坏行为。

  *

  * 所以,保证单例类只能被一个且只能是一个类加载器加载一次就能解决这个问题。为每个类构造器都增加一个这个检查机制就OK了。

  *

  * 防御示例:

  *

  * private static Class getClass(String classname) throws ClassNotFoundException{

  * ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

  * if(classLoader == null){

  * classLoader = SingleTon.class.getClassLoader();

  * }

  * return classLoader.loadClass(classname);

  * }

  *

  * 总结:如果你觉得懒汉式(线程安全)和双重校验锁的写法很相似,我不否认;但是在JDK5.0之前,它们的实际运行方式和得到的结果却相差很大。

  * 如果是在JDK5.0之前,其实前者会更安全,后者可能会不那么“安分”,很可能会出现非单例的情况,原因在于java的内存模型机制。

  * 在JDK5.0之前的java内存模型下,所谓的双重校验锁机制只是表象;当一个线程进入synchronized的锁环节并正在创建对象时,

  * 锁外的其他线程很有可能拿到这个正在创建的残缺的对象实例,因为此时的对象已经不是null了,这种情况根本就不是我们想要的。

  * 这里还涉及一个volatile关键字,这个关键字在JDK5.0以前的内存模型状态下就形同虚设,即时用了也不能保证双重校验锁出现上述状况。

  * 在JDK5.0及以后,java对内存模型进行了改进优化,这以后的volatile才真正起到了它该有的作用。双重校验锁才具有真正的多线程安全效果。

  *

  * 在开发中的建议:如果JDK版本是在5.0以及上时,使用双重校验锁和枚举的方式更安全高效,内部类的方式也可行,但是执行效率略低,毕竟是

  * “内部类”的方式,加载了两个类。如果是JDK5.0以下的,我个人可能会更倾向于饿汉式或者是内部类的方式,因为毕竟安全首要,效率其次。

  *

  * 不选懒汉式的原因是(线程安全),在都能保证安全的前提下,这种方式因为每个线程每次都要“握”一下锁(而实际上只需要第一次线程同步就可以了),

  * 这种情况下的效率比其他任何方式都要低至少2个级数(耗时方面)。

  *

  * 补充:内部类方式:因为内部类的属性都是静态的,只会在第一次类加载时加载,因而具有线程安全特性,这点跟饿汉式相同,不同的是,内部类方式

  * 的单例只会在真正调用的时候才会去new出一个单例,而饿汉式是一开始就new出单例放入内存。具体的使用可视对单例的LZ情况来确定。

  * LZ:即lazy,是否延迟生产实例。

  *