java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。
单例模式有一下特点:
1、单例类只能有一个实例。
2、单例类必须自己自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
正是由于这个特 点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或 文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。下面看看具体的单例代码实现。
1.饿汉式单例类
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton {
//私有的默认构造子
private Singleton() {}
//已经自行实例化
private static final Singleton single = new Singleton();
//静态工厂方法
public static Singleton getInstance() {
return single;
}
}
这种方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,因为导致类装载的原因有很多种,在单例模式中虽然大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,比如只是调用这个单例中的某个静态成员变量或方法,这时候都会初始化instance显然没有达到lazy loading的效果。还有如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用一块创建。
2.饿汉式单例类(变种)
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
表面上看起来与第一种差别挺大,其实实现方式差不多,都是在类初始化即实例化instance。
注意一个类(class)要被使用必须经过装载,连接,初始化这样的过程。
1 在装载阶段,类装载器(Bootstrap ClassLoader 或者用户自定义的ClassLoader) 把编译形成的class文件载入内存,创建类相关的Class对象,这个Class对象封装了我们要使用的类的类型信息,类最基本的加载规则是按需加载,即需要的时候才加载。
2 连接阶段又可以分为三个子步骤:验证、准备和解析。
1)验证就是要确保java类型数据格式 的正确性,并适于JVM使用。
2)准备阶段,JVM为静态变量分配内存空间,并设置默认值,注意,这里是设置默认值,比如说int型的变量会被赋予默认值0 。在这个阶段,JVM 可能还会为一些数据结构分配内存,目的 是提高运行程序的性能,比如说方法表。
3)解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。这个阶段也可以被推迟到初始化之 后,当程序运行的过程中真正使用某个符号引用的时候 再去解析它。
3 类会在首次被“主动使用”时执行初始化,为类(静态)变量赋予正确的初始值。在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化块给出的。而我们这里所说的主动使用包括:
1) 创建类的实例
2) 调用类的静态方法
3) 使用类的非常量静态字段
4) 调用Java API中的某些反射方法
5) 初始化某个类的子类
6) 含有main()方法的类启动时
注:访问静态常量(static final 修饰的)会存入调用类的常量池【这里说的是调用类main函数所在的类的常量池】,调用的时候本质上没有引用到 定义常量的类,而是直接访问了自己的常量池。所以,这里调用常量静态字段的时候,不会初始化定义类。
初始化一个类包括两个步骤:
1、 如果类存在直接父类的话,且直接父类还没有被初始化,则先初始化其直接父类
2、 如果类存在一个初始化方法,就执行此方法
注:初始化接口并不需要初始化它的父接口。
3 懒汉式单例类
//懒汉式单例类.在第一次调用的时候实例化
public class Singleton {
//私有的默认构造子
private Singleton() {}
//注意,这里没有final
private static Singleton single=null;
//静态工厂方法
public synchronized static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下它不需要同步,因为每次getInstace()时它都需要同步。
4 懒汉式双重校验锁(DCL)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这个是第三种方式的升级版,俗称双重检查锁定(DCL)。注意这里对singleton == null 有两次判空操作,为什么要这样呢,我们分析下:
假设线程A执行到singleton = new Singleton 这语句,这里看起来是一条代码,但是它实际上不是一个原子操作,这句代码会被编程成多条汇编指令,它大致做了下面三件事件:
1)给Singleton的实例分配内存;
2)调用Singleton的构造函数,初始化成员字段;
3)将singleton变量指向分配的内存空间(此时singleton就不是null了)。
但是由于java编译器允许处理器乱序执行,以及JDK1.5之前JMM(java memory model 即java内存模式)中Cache,寄存器到主内存回写顺序的规定,上面2,3步的执行顺序是无法保证的,也就是说,执行顺序有可能是 1 - 2 - 3 也有可能是1 - 3 - 2 。 如果是后者,并且在A线程执行完第3步后,2未执行之前,被切换到线程B上,这时候singleton变量因为A线程已经执行了第三步,singleton就不为空了,所以B线程就会直接取走singleton变量,再使用时就会出错,这就是DCL失效问题。
在JDK1.5后,SUN官方已经注意到这个问题,调整了JMM,具体化了volatile关键字,JDK1.5或之后的版本,只需要将singleton的定义时加上volatile关键字,可以保证singleton对象每次都从主内存中读取(不加volatile的话每个线程可以自己缓存变量),volatile关键字也可以保证“有序性”,即对一个变量的写操作先行发生于后面对这个变量的读操作。
DCL的优点是:资源利用率高,只有在使用getSingleton才会实例化对象,效率高。
5 静态内部类单例模式:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了classloader的机制来保证初始化instance时只有一个线程,它跟前面两种饿汉式不同的是(很细微的差别):前面两种只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,这个时候,这种方式相比前面的两种饿汉式方式就显得很合理。这里也不会有上面DCL的问题,所以是推荐使用的单例模式实现方式。
6 枚举单例
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,(注:序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例对象。即使是构造函数是私有的,反序列化时依然可以)不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
7 .登记式单例模式,使用容器实现
public class SingletonManager {
private static Map<String,Object> objectMap = new HashMap<>();
private SingletonManager(){}
public static void registerSingleton(String key,Object instance){
if(!objectMap.containsKey(key)){
objectMap.put(key,instance);
}
}
public static Object getInstance(String key){
return objectMap.get(key);
}
}
在程序的初始,将多种单例类型注入到一个统一的管理类中,使用时根据Key获取对应对象。这种方式使得我们可以管理多种类型单例。并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
总结
有两个问题需要注意:
1、如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类 装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2、如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
对第一个问题修复的办法是:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null){
classLoader = Singleton.class.getClassLoader();
}
return (classLoader.loadClass(classname));
}
对第二个问题修复的办法是:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() { }
private Object readResolve() {
return INSTANCE;
}
}