设计模式之单例模式

来源:互联网 发布:linux输入法切换快捷键 编辑:程序博客网 时间:2024/06/11 18:38

单例模式是应用最广泛的设计模式之一,这里我们使用3W原则来认识单例模式。

一、Who(什么是单例模式)

    一个类只有一个实例,并且只有一个全局获取入口。


二、Why(为什么要使用单例模式)

    1) 控制资源的使用,通过线程同步来控制资源的并发访问;
    2)控制实例产生的数量,达到节约资源的目的。
    3)作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。
    比如,数据库连接池的设计一般采用单例模式,数据库连接是一种数据库资源


三、How(如何实现单例模式)

    1、饿汉模式(线程安全)

/** * 单例模式(饿汉模式):在声明静态变量的时候,就初始化类实例(线程安全) * 1、私有化构造函数 * 2、创建一个静态的类实例变量 * 3、对外提供一个getInstance方法,返回2中的静态实例变量 */public class SingleTonOfHunger implements Serializable{    private static SingleTonOfHunger singleTonOfHungerInstance = new SingleTonOfHunger();    private SingleTonOfHunger () {}    public static SingleTonOfHunger getInstance() {        return singleTonOfHungerInstance;    }}


2、懒汉模式(线程不安全)   

/** * 单例模式(懒汉模式):在声明静态变量的时候,不初始化类实例,在获取单例的时候才初始化实例(延迟加载),当单例对象占用内存 * 过大时,可以提高性能,但是存在线程不安全的问题 * 1、私有化构造函数 * 2、创建一个静态的类实例变量 * 3、对外提供一个getInstance方法,返回2中的静态实例变量 */public class SingleTonOfLazy implements Serializable{    private static SingleTonOfLazy instance;    private SingleTonOfLazy() {        if (instance != null) {            //如果不是第一次构建,则直接抛出异常。不让创建(该方法可以防止反射破坏单例)            throw new RuntimeException();        }    }    public static SingleTonOfLazy getInstance() {        if(instance == null) {            instance = new SingleTonOfLazy();        }        return instance;    }    private Object readResolve() throws Exception {       return getInstance();    }}

饿汉模式是线程不安全的,如果要实现线程安全的的单例模式,可以给getInstance方法加上Synchronized同步关键字,但是由于直接在方法上使用synchronized关键字,即类锁同步,使得只能使用每次调用都要进行同步操作,性能比较低。所以引出一下DCL模式的单例实现


    3、DCL(Double CheckLock)

/** * 线程安全的单例模式:使用了DCL双检查锁机制 * 实现线程安全的单例模式还有以下方式: * 1、使用静态内部类实现单例模式 * * 2、序列化与反序列化模式的单例实现 * 序列化对象的hashCode和反序列化后得到的对象的hashCode值不一样,反序列化后返回的对象是重新实例化的,单例被破坏了 * 解决方案如下: * //该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉    protected Object readResolve() throws ObjectStreamException {        System.out.println("调用了readResolve方法!");        return MySingletonHandler.instance;    } * 3、使用静态代码块的方式 * 4、使用枚举数据类型实现单例模式:枚举类的构造方法在类加载是被实例化 */public class DoubleCheckSingleTon {    private static DoubleCheckSingleTon instance;    private DoubleCheckSingleTon(){}    /**     * 为什么不直接在getInstance方法上使用Synchronized同步,同步方法,粒度过大,影响性能。     * @return 返回双检查锁线程安全的单例模式     */    public static DoubleCheckSingleTon getInstance() {        if(instance == null) {            synchronized (DoubleCheckSingleTon.class) {  //降低同步的粒度,但此时存在线程不安全的问题,需使用双检查锁机制                if(instance == null) { //DCL双重检查锁                    instance = new DoubleCheckSingleTon();                }            }        }        return instance;    }}

通过两个判断,第一层是避免不必要的同步,第二层判断是否为null。
可能会出现DCL模式失效的情况。
DCL模式失效:
singleton=new Singleton();这句话执行的时候,会进行下列三个过程:
1.分配内存。
2.初始化构造函数和成员变量。
3.将对象指向分配的空间。
由于JMM(Java Memory Model)的规定,可能会出现1-2-3和1-3-2两种情况。
所以,就会出现线程A进行到1-3时,就被线程B取走,然后就出现了错误。这个时候DCL模式就失效了。
Sun官方注意到了这种问题,于是就在JDK1.5之后,具体化了volatile关键字(volatile修饰的变量、常量不会参与序列化),这时候只用调整一行代码即可。

private volatile static DoubleCheckSingleton instance;


4、静态内部类实现单例

public class StaticInnerClassSingleTon {        private StaticInnerClassSingleTon(){}        public static StaticInnerClassSingleTon getSingleton(){        return SingletonHolder.singleton;    }    private static class SingletonHolder{        private final static StaticInnerClassSingleTon singleton=new StaticInnerClassSingleTon();    }}

第一次加载Singleton类不会加载SingletonHolder类,但是调用getSingleton时,才会加载SingletonHolder,才会初始化singleton。即确保了线程安全,又保证了单例对象的唯一性,延迟了单例的实例化。这是最推荐的方式。


5、枚举模式实现单例

    通过枚举来写单例模式非常简单,什么都不用想,这是枚举的最大优点,

public enum Singleton{    singleton;    public void hello(){        System.out.print("hello");    }}

这样很简单,满足线程安全,并且避免了序列化和反射攻击。
除了枚举模式,其他模式在实现了Serializable接口后,反序列化时单例会被破坏。所以要重写readResolve()方法。

可能由于平时我们使用枚举都是为了表示类别,大家都很少使用这种方式去写单例模式。但是这确实是实现单例最简单,也比较推荐的方式。