线程安全的单例模式
1、双重检查锁定
原理:在getInstance()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。
public class SingletonDoubleCheck {
//TODO 注意如果不适用volatile 关键字无法保证线程安全
private volatile static SingletonDoubleCheck sDoubleCheck;
private SingletonDoubleCheck() {}
public static SingletonDoubleCheck getInstance() {
//第一次检查,不加锁
if(sDoubleCheck == null) {
System.out.println(Thread.currentThread()+" is null");
synchronized(SingletonDoubleCheck.class) {
//第二次检查,加锁情况下
if(sDoubleCheck == null) {
System.out.println(Thread.currentThread()+" is null");
//内存中分配空间 1
//空间初始化 2
//把这个空间的地址给我们的引用 3
sDoubleCheck = new SingletonDoubleCheck();
}
}
}
return sDoubleCheck;
}
}
比较老的方式,存在线程不安全问题,解决办法,加volatile关键字。
2、静态内部类实现单例模式(无锁方式)
原理:通过一个静态内部类定义一个静态变量来持有当前类实例,在类加载时就创建好,使用时获取。
缺点:无法做到延迟创建对象,在类加载时进行创建会导致初始化时间变长。
public class LazySingleton {
//TODO 通过private修饰,就是为了防止这个类被随意创建
private LazySingleton(){}
//TODO 利用虚拟机的类初始化机制创建单例
private static class SingletonHolder {
private static LazySingleton instance = new LazySingleton();
}
//TODO
public static LazySingleton getInstance() {
return SingletonHolder.instance;
}
}
3、懒汉式:
原理:类延迟创建,在单例类的内部由一个私有静态内部类来持有这个单例类的实例,延迟占位模式还可以用在多线程下实例域的延迟赋值。
缺点:由于synchronized的存在,多线程时效率很低。
public class LazySingletonSynchronized {
//TODO 通过private修饰,就是为了防止这个类被随意创建,只有内部自己可以创建
private LazySingletonSynchronized(){
System.out.println("LazySingleton is created!!");
}
// TODO 首先instance对象必须是private并且static,如果不是private那么instance的安全无法得到保证
//TODO 其次因为工厂方法必须是static,因此变量instance也必须是static
private static LazySingletonSynchronized instance = null;
// TODO 为了防止对象被多次创建,使用synchronized关键字进行同步(缺点是并发环境下加锁,竞争激励的场合可能对性能产生一定影响)
public static synchronized LazySingletonSynchronized getInstance() {
if(instance == null) {
instance = new LazySingletonSynchronized();
}
return instance;
}
}
4、饿汉式:
原理:在声明的时候就new这个类的实例,因为在JVM中,对类的加载和类初始化,由虚拟机保证线程安全,每次要用时直接返回。
缺点:无法做到延迟创建对象,在类加载时进行创建会导致初始化时间变长。
public class SingletonSynchronized {
//TODO 通过private修饰,就是为了防止这个类被随意创建,只有内部自己可以创建
private SingletonSynchronized(){ }
// TODO 首先instance对象必须是private并且static,如果不是private那么instance的安全无法得到保证
//TODO 其次因为工厂方法必须是static,因此变量instance也必须是static
private static SingletonSynchronized instance = new SingletonSynchronized();
public static SingletonSynchronized getInstance() {
return instance;
}
}
注意:上述4种方式实现单例模式的缺点
1)反序列化对象时会破环单例
反序列化对象时不会调用getXX()方法,于是绕过了确保单例的逻辑,直接生成了一个新的对象,破环了单例。
解决办法是:重写类的反序列化方法,在反序列化方法中返回单例而不是创建一个新的对象。
//反序列时直接返回当前INSTANCE
private Object readResolve() {
return INSTANCE;
}
2)在代码中通过反射机制,直接调用类的私有构造函数创建新的对象,会破环单例
解决办法是:维护一个volatile的标志变量在第一次创建实例时置为false;重写构造函数,根据标志变量决定是否允许创建。
private static volatile boolean flag = true;
private Singleton(){
if(flag){
flag = false; //第一次创建时,改变标志
}else{
throw new RuntimeException("The instance already exists !");
}
5、使用枚举
原理:定义枚举类型,里面只维护一个实例,以此保证单例。每次取用时都从枚举中取,而不会取到其他实例。
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
优点:
1)使用SingletonEnum.INSTANCE进行访问,无需再定义getInstance方法和调用该方法。
2)JVM对枚举实例的唯一性,避免了上面提到的反序列化和反射机制破环单例的情况出现:每一个枚举类型和定义的枚举变量在JVM中都是唯一的。
原因:枚举类型在序列化时仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
同时,编译器禁止重写枚举类型的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。