Java单例模式的几种写法

Java中单例模式的几种写法

在开发工程中,单例模式是最常用,也是最简单的一种设计模式。

学习新姿势


那么,一个完美的单例模式的实现需要做哪些事呢?

  • 单例
  • 延迟加载
  • 线程安全
  • 性能问题
  • 防止序列化问题
  • 防止反射攻击

以上是实现一个单例模式需要考虑到的一些基本因素。下面就这些因素来讲下几种单例模式的实现方法。


一、饿汉式

这是最常用,也是最直接了当的一种方法

public class SingletonFirst implements Serializable {
    //类加载时就创建实例
    private static SingletonFirst instance = new SingletonFirst();

    //私有化构造方法
    private SingletonFirst() {
        // 防止反射获取多个对象的漏洞
        if (null != instance) {
            throw new RuntimeException();
        }
    }

    //对外获取实例的方法
    public static SingletonFirst getInstance() {
        return instance;
    }

    /*
        防止反序列化获取多个对象的漏洞。
        无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
        实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
      */
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

在懒汉式中,当类加载时就创建了单例对象,这样不仅对性能有一定影响,更严重的是如果该单例对象的创建依赖于其他一些配置参数,而在该类加载时这些参数还未加载,那么就很可能造成单例对象创建失败,而且无法通过getInstance()方法传入参数,几时传入了也无效。当然,如果使用这种方式的话,线程绝对是安全的。如果通过反射创建对象的话,就会调用构造函数,此时在构造函数中判断单例对象是否为空,不为空的话就直接抛出异常。此外,readResolve()方法是所有序列化与反序列化都会调用的方法,在该方法中将单例对象直接返回,这样就能解决序列化问题了。


二、懒汉式

为了避免饿汉式的一系列缺点,于是选择使用懒汉式

public class SingletonSecond implements Serializable {
    private static SingletonSecond instance;

    private SingletonSecond() {
        // 防止反射获取多个对象的漏洞
        if (null != instance) {
            throw new RuntimeException();
        }
    }

    public static SingletonSecond getInstance() {
        if (null == instance) {
            instance = new SingletonSecond();
        }
        return instance;
    }

    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

在懒汉式中,多线程安全就是一个不得不考虑的问题了。如果在多线程中使用懒汉式,以上的getInstance()方法是一个线程不安全的方法。当然,可以直接加synchronized关键字来解决问题,如下所示

public static synchronized SingletonSecond getInstance() {
    if (null == instance) {
        instance = new SingletonSecond();
    }
    return instance;
}

这样就可以安全地解决线程问题了,但是,这样又带来了新问题,就是性能问题。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。代码如下

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

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

那么,该怎么解决呢??其实,只需要将 instance 变量声明成 volatile 就可以了。

// 声明成 volatile
private volatile static SingletonSecond instance;

//...

有人认为使用volatile的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。


静态内部类实现单例模式

使用静态内部类,能更好地实现单例模式,能同时解决线程安全问题和性能问题

public class SingletonFifth implements Serializable {

    private SingletonFifth() {
        // 防止反射获取多个对象的漏洞
        if (null != SingletonHolder.INSTANCE) {
            throw new RuntimeException();
        }
    }

    public static SingletonFifth getInstance() {
        return SingletonHolder.INSTANCE;
    }

    // 防止反序列化获取多个对象的漏洞
    private Object readResolve() throws ObjectStreamException {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final SingletonFifth INSTANCE = new SingletonFifth();
    }
}

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


枚举实现单例模式

这是最快捷的单例模式实现方式

public enum SingletonSixth {
    INSTANCE;

    public void doSomething() {
        //...
    }
}

可以通过SingletonSixth.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,而且还能防止反序列化导致重新创建新的对象。但是这种方法有一个比较致命的缺陷,就是在获取单例对象时无法传入参数,而之前的所有方法(饿汉式除外)都可以在getInstance(Object... args)方法中传入一些初始化参数。


总结

一般来说,单例模式有四种写法:懒汉(包含双重检验锁)、饿汉、静态内部类、枚举。个人见解而已,不喜勿喷!!

你可能感兴趣的:(JavaSE)