单例模式的几种写法
1、懒汉模式
懒汉模式,顾名思义就是很懒,要等到第一次调用的时候才创建。
public class Lazy { private static Lazy INSTANCE = null; private Lazy() { } public static Lazy getInstance() { if (INSTANCE == null) { INSTANCE = new Lazy(); } return INSTANCE; } }
看代码,可以看出INSTANCE
在创建的时候是线程不安全的,在多线程情况下会创建多个Lazy
对象。
2、加锁的懒汉模式
既然懒汉模式线程不安全,那就给它加个锁来保证线程安全。
public class Lazy { private static Lazy INSTANCE = null; private Lazy() { } public static Lazy getInstance() { if (INSTANCE == null) { synchronized (Lazy.class) { if (INSTANCE == null) { INSTANCE = new Lazy(); } } } return INSTANCE; } }
在第一次创建的时候加个synchronized
同步。那这样就没有问题了吗?
我们来看看上面的new Lazy()
在JVM
中发生了什么。理想的代码如下:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 INSTANCE = memory; //3:使INSTANCE指向刚分配的内存地址
CPU
会将不满足happens-before
原则的指令进行重排——指令重排(Google一下就知道了)。所以上述指令可能会被分配成:
memory = allocate(); //1:分配对象的内存空间 INSTANCE = memory; //3:使INSTANCE指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象
上面代码可以看出,INSTANCE
指向内存地址比初始化对象先行发生了,这样导致INSTANCE
的值不为null
。如果此时有另一个线程走到if (INSTANCE == null)
判断时,此时INSTANCE
不为空,但是未进行初始化,从而导致系统异常。
如果能让CPU
不对指令进行重排?使用volatile
关键字,可以阻止写操作指令重排。
3、双重锁的懒汉模式
public class Lazy { private volatile static Lazy INSTANCE = null; private Lazy() { } public static Lazy getInstance() { if (INSTANCE == null) { synchronized (Lazy.class) { if (INSTANCE == null) { INSTANCE = new Lazy(); } } } return INSTANCE; } }
4、饿汉模式
public class Hungry { private static Hungry INSTANCE = new Hungry(); private Hungry() { } public static Hungry getInstance() { return INSTANCE; } }
饿汉非常饿,所以在类的加载过程中就已经把对象实例好了。在类加载过程中,classloader
机制保证了加载类只有一个线程,这就意味着只有一个线程去创建实例,这种模式是天然的线程安全,但是也存在一个资源浪费的问题,如果INSTANCE
一直没有使用,则是浪费。
5、 静态内部类模式
public class InnerStaticClass { private static class Holder { private static final InnerStaticClass INSTANCE = new InnerStaticClass(); } private InnerStaticClass() { } public static InnerStaticClass getInstance() { return Holder.INSTANCE; } }
该模式与饿汉模式的区别在于用静态内部类来实例化单例,而静态内部类的加载是在第一次显示调用的时候才会进行加载,因此可以解决INSTANCE
一直没有使用而带来的资源浪费问题。
6、 枚举模式
public enum EnumInstance { INSTANCE; public void printOut(){ System.out.println("枚举模式"); } }
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 《Effective Java》 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。