单例模式

2016/08/16 设计模式

wiki:

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

构建方式:

懒汉方式

指全局的单例实例在第一次被使用时构建。

public class Singleton {

    private static Singleton INSTANCE;

    /**
     * 构造器私有
     */
    private Singleton() {
    }


    /**
     * 高并发下会出现多个实例,线程不安全
     *
     * @return 单例实体
     */
    /*public static Singleton getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }*/

    /**
     * 简单改进,在方法上加上synchronized,这样会非常影响效率,导致过多的线程等待.
     */
    /*public static synchronized Singleton getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }*/

    /**
     * double check,在实例不等于null时直接获取,避免了同步。
     * double check 原因: 假设我们去掉同步块中的是否为null的判断,
     * 有这样一种情况,假设A线程和B线程都在同步块外面判断了INSTANCE为null,
     * 结果A线程首先获得了线程锁,进入了同步块,然后A线程会创造一个实例,
     * 此时INSTANCE已经被赋予了实例,A线程退出同步块,直接返回了第一个创造的实例,
     * 此时B线程获得线程锁,也进入同步块,此时A线程其实已经创造好了实例,
     * B线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,
     * 直接就是一条创建实例的语句,所以B线程也会创造一个实例返回,此时就造成创造了多个实例的情况。
     * 经过刚才的分析,貌似上述双重加锁的示例看起来是没有问题了,但如果再进一步深入考虑的话,其实仍然是有问题的。
     * 如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的。
     * 因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,
     * 也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,
     * 但是有些情况下是会造成莫名的错误。
     * 首先要明白在JVM创建新的对象时,主要要经过三步。
     * <p>
     * 1.分配内存
     * 2.初始化构造器
     * 3.将对象指向分配的内存的地址
     * <p>
     * 这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将
     * 内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码
     进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。
     * 因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给INSTANCE,
     然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,
     会认为INSTANCE对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,
     这个线程使用了INSTANCE,就会产生莫名的错误。
     * <p>
     * <p>
     * 解决办法:使用 volatile 关键字约束INSTANCE,使之可见,保证对它的所有读写操作绑定成一个不可拆分的动作
     */
    /*public static Singleton getInstance() {
        if (null == INSTANCE) {
            synchronized (Singleton.class) {
                if (null == INSTANCE) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }*/
}

以上的懒汉式实现方式都存在问题,那么懒汉式标准实现模式如下:
/**
 * 标准懒汉式单例模式:
 * 因为静态的只初始化一次,在类第一次被加载的时候,保证单例,没有并发问题,
 * 而且只有在调用getInstance时创建实例;
 * 其他推荐写法:利用枚举 @EnumSingle <<effective java>>中这么说 : 单元素的枚举类型已经成为实现Singleton的最佳方法
 * 其他不推荐写法:饿汉式 @HungrySingleton 缺点:没经过内部类处理,一旦访问任何HungrySingleton静态域就会创建实例,
 * 但是也许我们从始至终都不会用到它;存在序列化问题,如果实现Serializable,反序列化时单例会被破坏!--->解决办法:
 * 重写readResolve
 * private Object readResolve() throws ObjectStreamException {
 * return INSTANCE;
 * }
 */
public class Single {
    private Single() {
    }

    public static Single getInstance() {
        return Single.InnerSingle.instance;
    }

    private static class InnerSingle {
        static Single instance = new Single();
    }
}

饿汉方式

指全局的单例实例在类装载时构建。

public class HungrySingleton implements Serializable {
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return INSTANCE;
    }

    /**
     * 如果实现了Serializable, 必须重写这个方法
     */
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

其他实现方式ENUM

public enum EnumSingle {
    INSTANCE;

    private Resource resource;

    EnumSingle() {
        resource = new Resource();
    }

    public Resource getInstance() {
        return resource;
    }

}

Search

    Post Directory