一般单例都是五种写法:懒汉、饿汉、双重校验锁、静态内部类和枚举。为了记录学习的过程,这里整理了几种常见的单例写法。
青铜5:(Lazy-loaded,但线程不安全)
public class Singleton { private static Singleton instance; private Singleton(){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这段代码简单明了,而且实现了延迟加载,但是在多线程环境下,如果多个线程同时调用 getInstance() 方法,可能会发生多个线程同时通过(instance==null)的条件检测,从而进入if语句的程序代码块创建出多个实例。
懒汉式:synchronized(Lazy-loaded,线程安全,但不高效)
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized),使得只要是进入getInstance() 方法的线程都得同步。实际上,创建实例对象的动作只有一次,后面的方法调用都是读取成员变量,对后续的读操作也进行线程同步会严重影响性能。
饿汉式:static final field(非Lazy-loaded)
这种方法非常简单,因为单例的实例被声明成 static final,在第一次加载类到内存中时就会初始化,创建实例对象也是线程安全的(由JVM实现保证)。
public class Singleton{ //类加载时就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ // Singleton with static factory return instance; } }
它没有实现延迟加载,即使没有调用getInstance()方法,instance也会在类加载后就被初始化。这会导致一些使用限制:譬如 Singleton 实例的创建依赖配置文件或者参数,必须在执行 getInstance() 之前调用某个方法设置参数给它,那样这种单例写法就无法使用了。类似的方法还有:
public class Singleton{ public static final Singleton instance = new Singleton(); // Singleton with public final field private Singleton(){} } // <Effective Java>第14页讲诉了两者的差异
双重检验锁 + volatile(Lazyload,线程安全,但晦涩)
所谓“双重检查加锁”指的是:
并不是每次进入getInstance()方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来就只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。
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 件事情:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址
但是在 JVM 的JIT编译器中存在指令重排序的优化。也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是 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; } }
但是特别注意在 Java 1.5 以前的版本使用了 volatile 的双检锁还是有问题的,这个问题在 Java 1.5 中才得以修复,所以在这之后才可以放心使用 volatile。
静态内部类:IoDH,initialization-on-demand holder
这个模式综合使用了Java的静态内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
public class Singleton { private Singleton() {} private static class LazyHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { // From wikipedia return LazyHolder.INSTANCE; } }
静态内部类相当于其外部类的static部分,只有在第一次被使用的时候才会被装载。它的对象不依赖于外部类对象(实例),因此可以直接创建。
多线程缺省同步锁
大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不需要手动进行同步控制了。这些情况包括:
1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
2.访问final字段时
3.在创建线程之前创建对象时
4.线程可以看见它将要处理的对象时
枚举 Enum
从Java 1.5起,只需编写一个包含单个元素的枚举类型:
public enum Singleton { INSTANCE; }
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面向复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型以及成为实现Singleton的最佳方法。
解决方案的思路:
- 类级(静态)内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。
--------------------以下是几个存疑的细节--------------------
1.static final到底有哪些细节
2.static field处的赋值初始化到底和static代码块有先后吗?
3.P50 in Java EE设计模式解析与应用中的例子真的有Lazyload效果吗?
4.单例模式的其他问题《深入浅出单实例》