java单例模式分析

作用

单例模式(Singleton):保证一个类仅有一个实例,并提供一个访问它的全局访问点

适用场景

  1. 应用中某个实例对象需要频繁的被访问。

  2. 应用中每次启动只会存在一个实例。如数据库系统。

使用方式

  1. 懒汉式
public class Singleton {  
    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
    private static Singleton instance = null;  
    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  
    /* 懒汉式,静态工程方法,创建实例 */  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}  
	

调用:

Singleton.getInstance() ;

优点:延迟加载(需要的时候才去加载) 缺点: 线程不安全,在多线程中很容易出现不同步的情况,如在数据库对象进行的频繁读写操作时。

2.懒汉式变种,加同步锁

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

更一般的写法是这样

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

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。 在Android源码中使用的该单例方法有:InputMethodManager,AccessibilityManager等都是使用这种单例模式。

3.懒汉式变种,双重检验锁 (Double Check Locking) (DCL)

public class Singleton {    
   private static Singleton instance = null;    
  // 只在第一次初始化的时候加上同步锁*/ 
  public static Singleton getInstance() {  
      if (instance == null) {  
          synchronized (Singleton.class) {  
              if (instance == null) {  
                  instance = new Singleton();  
              }  
          }  
      }  
      return instance;  
  }  
}   

这种方法貌似很完美的解决了上述效率的问题,它或许在并发量不多,安全性不太高的情况能完美运行,但是,这种方法也有不幸的地方。问题就是出现在这句

instance = new Singleton(); 

在JVM编译的过程中会出现指令重排的优化过程,这就会导致当 instance实际上还没初始化,就可能被分配了内存空间,也就是说会出现 instance !=null 但是又没初始化的情况,这样就会导致返回的 instance 不完整。

我们来看看这个场景:假设线程一执行到instance = new SingletonKerrigan()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情: 1.给Kerrigan的实例分配内存。 2.初始化Kerrigan的构造器 3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。 但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来。 DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。

在android图像开源项目Android-Universal-Image-Loader (https://github.com/nostra13/Android-Universal-Image-Loader) 中使用的是这种方式。

4、饿汉式

public class Singleton {    
    private static SingletonD instance = new Singleton();    
    public static Singleton getInstance() {    
        return instance;    
    }    
}   

这种写法不会出现并发问题,但是它是饿汉式的,在ClassLoader加载类后Kerrigan的实例就会第一时间被创建,饿汉式的创建方式在一些场景中将无法使用:譬如实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

5、静态内部类

public class Singleton {  
    private static class SingletonHolder {  
        private static Singleton instance = new Singleton();  
    }  
    /** * 私有的构造函数 */  
    private Singleton() {  
    }  
    public static Singleton getInstance() {  
        return SingletonHolder.instance;  
    } 
}  

这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。

这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式就显得很合理。

6、枚举

public enum SingletonEnum {  
    /** * 1.从Java1.5开始支持; * 2.无偿提供序列化机制; * 3.绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候; */  
    instance;  
    private String others;  
    SingletonEnum() {  
       } 
    }
 

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

总结

创建单例实例的方式有: 1.直接new单例对象 2.通过反射构造单例对象 3.通过序列化构造单例对象。

对于第一种情况,一般我们会加入一个private或者protected的构造函数,这样系统就不会自动添加那个public的构造函数了,因此只能调用里面的static方法,无法通过new创建对象。

对于第二种情况,如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类 装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。修复的办法是:

     private static Class getClass(String classname)      
                                         throws ClassNotFoundException {     
       ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
      if(classLoader == null)     
           classLoader = Singleton.class.getClassLoader();     
      return (classLoader.loadClass(classname));     
    }     
}  
  

对于第三种情况,如果单例对象有必要实现Serializable接口(很少出现),则应当同时实现readResolve()方法来保证反序列化的时候得到原来的对象。写法如下:

public class Singleton implements Serializable {    
    private static class SingletonHolder {    
       /** * 单例对象实例 */    
        static final Singleton INSTANCE = new Singleton();    
    }    
    public static Singleton getInstance() {    
       return SingletonHolder.INSTANCE;    
    }    
   /** * private的构造函数用于避免外界直接使用new来实例化对象 */    
    private Singleton() {    
    }    
    /** * readResolve方法应对单例对象被序列化时候 */    
    private Object readResolve() {    
        return getInstance();    
   }    
}    
    
    原文作者:mxn原创
    原文地址: http://souly.cn/%E6%8A%80%E6%9C%AF%E5%8D%9A%E6%96%87/2015/05/28/java%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E5%88%86%E6%9E%90/
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞