1. 懒汉式
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
分析:
懒汉式给getInstance方法加上了同步锁,解决了多线程的情况下可能创建多个实例的问题。但是每次调用getInstance方法都有一个获取/释放同步锁的过程。加锁是很耗时的,这是一种低效率的实现方式。
结论:
理论上是正确的单例实现方式,但效率不高,不推荐使用。
2. 双重校验锁定
双重校验锁定又分为两种:
2.1 synchronized方式
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
2.2 Lock方式:
import java.util.concurrent.locks.ReentrantLock;
public class Singleton {
private static volatile Singleton INSTANCE = null;
private static ReentrantLock lock = new ReentrantLock();
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
try {
lock.lock();
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
} finally {
lock.unlock();
}
}
return INSTANCE;
}
}
分析:
双重校验锁定其实原理与懒汉式一样,是懒汉式的优化版本。我们知道,getInstance方法可能被调用很多次,但实例仅需要创建一次,后面直接返回第一次创建的实例即可。直接对getInstance方法加同步锁并不划算,要满足仅创建一个实例的需求,只需要对创建实例的过程进行加锁即可。只有当INSTANCE为null时,需要获取同步锁,创建一个实例。实例创建之后,获取实例无需再获取锁。
值得注意的是INSTANCE需要用volatile关键字修饰,这里volatile的作用是保证可见性和禁止指令重排。
INSTANCE = new Singleton();是一个非原子操作,编译后会生成多条字节码指令,可能会出现指令重排,假设编译后生成a,b,c三条指令:
a. memory = allocate();//开辟内存空间
b. ctorInstance(memory);//实例初始化
c. INSTANCE = memory;//引用赋值,使INSTANCE指向开辟的内存地址。
我们期望的执行顺序应该是:a, b, c,但是由于JVM运行时可能发生指令的重排序,可能会出现的执行顺序:a, c, b
假定有两个线程1、2,
指令重排的情况(a, c, b顺序执行):
如果线程1获取到锁进入创建实例,执行了指令a和c,此时线程2刚好进入第一个if判断语句,由于此时INSTANCE已经不为 null,线程2可以访问该实例引用指向的地址,但实际上由于指令b还未执行,实例还未初始化,这块内存空间仅仅开辟了,但是还没有任何有意义的内容,线程2的访问就有可能出现异常。
禁止指令重排的情况(a,b,c顺序执行):
禁止指令重排后,只能按照顺序a,b,c执行。如果线程1获取到锁进入创建实例,执行了指令a和b,线程2进入第一个if判断语句,由于此时INSTANCE为 null,线程2就必须等待线程1的指令c执行完后释放锁,然后线程2进入第二if判断语句,此时INSTANCE已经不为 null,并且实例也初始化好了。
为什么指令a不会被重排到指令b,c后面?这是因为:
Java 语言规定了线程执行程序时需要遵守 intra-thread semantics规则。
这个规则保证指令重排不会改变单线程内的程序执行结果。因为指令b,c都是依赖指令a的执行后才有内存地址的,所以按照单线程的自然顺序,指令a永远在b,c前面。
综上,所以需要加volatile关键字禁止指令重排序实现线程安全的单例。
如果一定要说双重校验锁定有什么缺点的话那就是:写法稍显复杂,容易写错。
结论:
双重校验锁定是一种高效并且实现了lazy loading的单例实现方式,推荐使用。
3. 饿汉式
public class Singleton {
private static Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
分析:
饿汉式利用了JVM的类加载机制的特性:
类加载的初始化阶段是线程安全的,同一个类加载器下,一个类型只会初始化一次。
类中的所有静态变量的赋值动作和静态语句块的执行都是在初始化阶段进行的。因此,在饿汉式实现中,创建Singleton实例并给静态变量INSTANCE赋值是线程安全的。通过这个机制,在Singleton类被加载的时候就早早的创建了实例。即在调用getInstance的方法之前,就已经创建好了实例。饿汉式简单、安全、可靠,缺点是在还不需要实例的时候就已经创建了实例,没有实现lazy loading,降低了内存的使用效率。
结论:
饿汉式是一种简单、安全、可靠,但没有实现lazy loading的单例实现方式,实例占用内存不大时,可以使用。
4. 静态内部类
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
分析:
静态内部类式巧妙地利用了JVM的类加载机制的两个特性:
a. 类加载的初始化阶段是线程安全的。
b. 类只有在首次被用到的时候才会被加载(首次使用new或者反射创建实例、调用该类静态方法、静态属性、初始化其子类等等才会加载)。
在Singleton类被加载的时候其内部类SingletonHolder并不会加载,在调用getInstance()方法时用到了SingletonHolder才进行SingletonHolder类的加载,这时去创建Singleton的实例就实现了lazy loading。
结论:
静态内部类式是一种安全、可靠、简单,并且实现了lazy loading的单例实现方式,推荐使用。
题外话
我们还需要其它写法吗?双重校验锁和静态内部类似乎已经很完美了。但是,在考虑反射和序列化的情况下,前面的四种实现方式都存在问题。
首先,我们先看反射:
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}
我们看上面这段代码。考虑到反射以后,私有构造函数似乎并不能确保只有一个实例。外部调用者仍能不调用getInstance方法而通过反射创建一个实例。
然后,我们再看序列化:
import org.apache.commons.lang3.SerializationUtils;
public class Test {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = SerializationUtils.deserialize(serialize);
System.out.println(instance == newInstance);
}
}
同样的,考虑到序列化以后,外部调用者也能通过序列化创建一个实例。还有,clone方法也是一样的问题。
所以,我们还需要一种能防反射攻击和序列化攻击的单例模式实现,这就是枚举。
5. 枚举
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
调用:
public class Test {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
}
分析:
这种写法主要是利用了枚举的特性:
a. 枚举类的实例必须在枚举类中显式的指定(首行开始的以第一个分号结束)
b. 除了在枚举类中显式指定的实例外, 没有任何方式(new,clone,反射,序列化)可以手动创建枚举实例。
枚举类的构造方法是私有的,保证外部不能通过new创建枚举的实例。clone、反射、序列化都会抛出异常(java枚举(enum)全面解读),这样又保证了外部不能通过clone、反射、序列化创建实例。和饿汉式和静态内部类相似,枚举类的实例实际上是通过静态代码块创建,由类的加载机制保证线程安全。需要注意的是,虽然枚举实现单例足够安全,但由于其也是在枚举类加载的初始化阶段就创建了实例,实际上是一种安全性加强的饿汉式单例模式,也存在内存使用效率不够高的问题。
结论:
枚举类是一种非常安全可靠、实现简单的单例实现方式,推荐使用。
6. CAS
public class Singleton {
private static final AtomicReference INSTANCE = new AtomicReference();
private Singleton() {
}
public static Singleton getInstance() {
for (; ; ) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
分析:
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。
结论:
由于笔者对于CAS了解不深,CAS部分基本全部拷贝原文(不使用synchronized和lock,如何实现一个线程安全的单例?),但是这里还是勉强说一下个人见解,不一定准确,谨慎采纳:CAS是用于高并发的单例实现方式,高并发意味着需要更高效的CPU和更宽裕的内存。如果是移动端应用采用CAS单例实现方式,一旦出现死循环或者OOM,影响将是灾难性的。除非使用场景真的需要高并发,否则建议慎重考虑使用CAS方式实现单例。
7.总结
讨论完了6种不同的实现方式,我们总结下其中比较好的单例实现方式的使用场景:
1. 实例占用内存大需要延迟加载,可以使用双重校验锁定和静态内部类,推荐使用静态内部类。
2. 实例占用内存小或者需要防反射、防序列化、防clone,使用枚举。
3. 高并发,使用CAS。
8. 参考
Java单例模式:为什么我强烈推荐你用枚举来实现单例模式
【单例深思】饿汉式与类加载
java枚举(enum)全面解读
双重检查锁单例模式为什么要用volatile关键字?
不使用synchronized和lock,如何实现一个线程安全的单例?
感谢几位原作者辛勤付出。