设计模式之单例模式

来源:互联网 发布:eclipse创建Java步骤 编辑:程序博客网 时间:2024/06/06 04:55

定义

    单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。


作用

    1.对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
    2.由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。


Java语言实现

饿汉式

public class Singleton {    private Singleton() {}    private static final Singleton instance = new Singleton();    public static Singleton getInstance() {        return singleton;    }}

    上述代码唯一的不足是无法对 instance 实例做延时加载,例如单例的创建过程很慢,而由于 instance 变量是 static 定义的,因此在 JVM 加载单例类时,单例对象就会被建立,如果此时这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。


懒汉式,线程不安全

public class Singleton {    private static Singleton instance;    private Singleton() {    }    public static Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance;    }}

    这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护 getInstance() 方法。如果不保护 getInstance() 方法,则可能返回Singleton 对象的两个不同的实例。


懒汉式,线程安全

public class Singleton {    private static Singleton instance;    private Singleton() {    }    public synchronized static Singleton getInstance() {        if (instance == null) {            instance = new Singleton();  //2        }        return instance;    }}

    代码针对多线程访问 getInstance() 方法运行得很好。然而,当分析这段代码时,您会意识到只有在第一次调用方法时才需要同步。由于只有第一次调用执行了 //2 处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其他调用用于决定 instance 是非 null 的,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是synchronized 的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。这就引出了双重检验锁。


双重检验锁

public static Singleton getSingleton() {    if (instance == null) {                         //Single Checked        synchronized (Singleton.class) {            if (instance == null) {                 //Double Checked                instance = new Singleton();            }        }    }    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 就可以了。

public class Singleton {    private volatile static Singleton instance; //声明成 volatile    private Singleton (){}    public static Singleton getSingleton() {        if (instance == null) {                                     synchronized (Singleton.class) {                if (instance == null) {                           instance = new Singleton();                }            }        }        return 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。

    相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。


静态内部类 static nested class

    这种方法也是《Effective Java》上所推荐的。

public class Singleton {      private static class SingletonHolder {          private static final Singleton INSTANCE = new Singleton();      }      private Singleton (){}      public static final Singleton getInstance() {          return SingletonHolder.INSTANCE;     }  }

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


枚举 Enum

    用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum EasySingleton{    INSTANCE;}

    我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

参考资料

1.Java单例模式中双重检查锁的问题
2.正确使用双重检查锁(DCL)
3.双重检查锁失效是因为对象的初始化并非原子操作?
4.从Java实现Singleton模式的一个bug到Java内存模型

0 0