设计模式(四)Singleton设计模式

1,104 阅读6分钟

Singleton单例模式是最简单的设计模式,它的主要作用是保证在程序运行生命周期中,使用了单例模式的类只能有一个实例对象存在。单例模式实现了类似C语言中全局变量的功能,单例模式常用于注册/查找的服务。

单例模式的UML图如下:

单例模式有两种实现方式:饱汉模式和饿汉模式,如下:

1.饱汉单例模式例子代码:

public class Singleton1{

    //饱汉模式,声明时就创建实例对象
    public static final Singleton1 instance = new Singleton1();

    //单类模式的构造方法必须为private,以避免通过构造方法创建对象实例,
    //并且必须显示声明构造方法,以防止使用默认构造方法
    private Singleton1(){}

    //单类模式必须对外提供获取实例对象的方法
    public static Singleton1 geInstance(){
        return instance;
    }

}
2.饿汉单例模式即延迟初始化单例方式,例子代码:


public class Singleton2{

    //饿汉模式,声明时不创建实例对象
    public static Singleton2 instance;

    //单类模式的构造方法必须为private,以避免通过构造方法创建对象实例,
    //并且必须显示声明构造方法,以防止使用默认构造方法
    private Singleton2(){}

    //单类模式必须对外提供获取实例对象的方法,延迟初始化的单类模式必须使用synchronized同步关键字,否则多线程情况下很容易产生多个实例对象
    public static synchronized Singleton2 geInstance(){
        //延迟初始化,只有当第一次使用时才创建对象实例
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}

一般认为饱汉模式要比饿汉模式更加安全。

上面两种Singleton单例设计模式的实现方式都隐藏有如下的问题:

(1).虽然构造方式的访问修饰符为private,即除了自身以外其他任何类都无法调用,但是通过反射机制的setAccessiable(true)方法可以访问私有方法和属性。因此Singleton单例模式必须考虑这种例外情况。

(2).对象序列化之后再反序列化时会生成新的对象,因此当Singleton单例模式类实现序列化接口时,必须显式声明所有的字段为tranisent,并且提供如下的readResolve方法来防止通过序列化破坏单态模式:



private Object readResolve(){
    return INSTANCE;
}

3.使用Lazy initialization holder class模式实现单态:


public class Singleton3 {  

    /** 
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 
     * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载 
     */  
    private static class SingletonHolder{   
        //静态初始化器,由JVM来保证线程安全 
        private static Singleton3 instance = new Singleton3();  
    }  

    //私有化构造方法  
    private Singleton3(){  
    }  

    public static  Singleton3 getInstance(){  
        return SingletonHolder.instance;  
    }  
}


当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会被虚拟机在装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。 
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。 

4.在JDK1.5之后引入了Enum枚举,因此在JDK1.5之后Singleton单例模式又有了第三种实现方式,也是最好的实现方式,例子如下:



public enum Singleton4{
    INSTANCE{
        public void doSomething(){
            ...
        }
    };
    public abstract void doSomething();  
}

Singleton单例模式中只有一个INSTANCE枚举元素,枚举可以保证真个程序生命周期中只有一个实例对象存在,同时还避免了常规Singleton单例模式private构造方法被反射调用和序列化问题(枚举提供了序列化保证机制,确保多次序列化和反序列化不会创建多个实例对象)。
注意:java中除了构造方法可以创建对象实例以外,还可以通过克隆方法(clone()是Object中的protected方法)来创建对象,若单例对象直接继承自Object对象,则如果没有提供具体clone方法实现,则当调用克隆方法创建对象时,会抛出运行时的异常CloneNotSupportedException。

若单例类继承了实现克隆方法的类,则在单例类中必须覆盖父类的克隆方法,显式抛出异常CloneNotSupportedException。

另外,实现了单例模式的类不能再有派生子类,因为构造方式是私有的,子类无法调用父类构造方法,因此达到了Final的效果。

JDK的中单例模式的应用:
java.lang.Runtime

单例模式的优点:
* 在内存中只有一个对象,节省内存空间。

* 避免频繁的创建销毁对象,可以提高性能。

* 避免对共享资源的多重占用。

* 可以全局访问。

适用场景:由于单例模式的以上优点,所以是编程中用的比较多的一种设计模式。我总结了一下我所知道的适合使用单例模式的场景:

* 需要频繁实例化然后销毁的对象。

* 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。

* 有状态的工具类对象。

* 频繁访问数据库或文件的对象。

* 以及其他我没用过的所有要求只有一个对象的场景。

单例模式注意事项:

* 只能使用单例类提供的方法得到单例对象,不要使用反射,否则将会实例化一个新对象。

* 不要做断开单例类对象与类中静态引用的危险操作。

* 多线程使用单例使用共享资源时,注意线程安全问题。

关于java中单例模式的一些争议:

单例模式的对象长时间不用会被jvm垃圾收集器收集吗?

看到不少资料中说:如果一个单例对象在内存中长久不用,会被jvm认为是一个垃圾,在执行垃圾收集的时候会被清理掉。对此这个说法,我持怀疑态度我的观点是:在hotspot虚拟机1.6版本中,除非人为地断开单例中静态引用到单例对象的联接,否则jvm垃圾收集器是不会回收单例对象的。


在一个jvm中会出现多个单例吗?

在分布式系统、多个类加载器、以及序列化的的情况下,会产生多个单例,这一点是无庸置疑的。那么在同一个jvm中,会不会产生单例呢?使用单例提供的getInstance()方法只能得到同一个单例,除非是使用反射方式,将会得到新的单例。代码如下



Class c = Class . forName ( Singleton . class . getName ( ) ) ;
Constructor ct = c . getDeclaredConstructor ( ) ;
ct . setAccessible ( true ) ;
Singleton singleton = ( Singleton ) ct . newInstance ( ) ;

note:免费的才是最贵的