单例模式实现详解
单例模式的四大原则:
- 私有构造方法
- 以静态方法或者枚举返回实例
- 确保实例只有一个,尤其在多线程环境下
- 确保反序列化不会创建新的实例
常用单例模式构造方法
饿汉模式
public class SingletonTest {
private static SingletonTest INSTANCE = new SingletonTest();
private SingletonTest() {}
public static SingletonTest getInstance(){
return INSTANCE;
}
}
饿汉模式在类初始化时就在内存中创建对象,以空间换时间,不存在线程安全问题。
懒汉模式
public class SingletonTest2 {
private static SingletonTest2 INSTANCE = null;
private SingletonTest2(){}
private static SingletonTest2 getInstance(){
return new SingletonTest2();
}
}
饿汉模式在静态方法被调用时创建实例,已时间换空间,存在线程安全问题。当然在多线程环境下,实现一个大型的实例单例化时存在风险,故我们接下来将讨论如何使得懒汉模式在多线程条件下使用。
双重锁懒汉模式(Double Check Lock)
public class SingletonTest3 {
private static SingletonTest3 INSTANCE = null;
private SingletonTest3(){}
private static SingletonTest3 getInstance(){
if(INSTANCE == null){
synchronized (SingletonTest3.class){
if(INSTANCE == null){
INSTANCE = new SingletonTest3();
}
}
}
return INSTANCE;
}
}
DCL模式无疑是解决懒汉模式线程安全问题的方法之一,DCL的优点是只有在对象初始化时被创建,首次判断实例为空是为了避免盲目加锁,当第一次加载方法时,对实例进行加锁和实例化,既节省了时间也节省了空间,但由于JVM存在乱序执行的优化策略,往往使得DCL也存在线程不安全问题
举个荔枝:
INSTANCE = new SingletonTest3();
这句话在执行时其实JVM分为三步 去创建对象
- 堆内存开辟内存空间
- 实例化类方法,变量
- 将对象指针指向推内存地址
但是在2未执行完成时,jvm有可能先执行3步骤,使得新线程来时,对象实例不为空将实例返回,使得对象存在问题DCL存在不安全
解决方法
在jdk1.5之后官方具体化了volatile 使用关键字修饰既可保证DCL安全问题
private volatile static SingleTon INSTANCE = null;
静态内部类模式
public class SingletonTest4 {
private SingletonTest4(){}
private static class SingletonInstance{
private static SingletonTest4 INSTANCE = new SingletonTest4();
}
public static SingletonTest4 getInstance(){
return SingletonInstance.INSTANCE;
}
}
静态内部类的形式既可以使得空间节省,也可以使得内存节省,外部类加载时并不需要直接加载内部类,而是在访问静态方法时才去加载内部类。
那么静态内部类如何保证线程安全性呢?首先了解类的加载机制。
- 遇到new ,getstatic,setstatic,invokestatic字节码指令时类加载。
- 使用反射机制java.lang.reflect
- 实例化子类时,加载父类
- 当方法遇到java.lang.invoke.MethodHandle 句柄时,为初始化,就会执行初始化操作。
当类对象执行静态方法获取实例对象时,会执行实例化静态内部类 使得实例方法只被new 一次而不会多次去创建 避免多线程情况下不安全的情况。当getInstance()方法被调用时,SingletonInstance才在SingleTon4的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。jvm在实例化类时会调用方法,在类对象实例化时,jvm会保证对象加锁,同步保证同一时间只有一个线程调用方法,其他线程进行实例化是阻塞线程,保证线程安全性。
枚举方式
public enum SingleTonEnum {
INSTANCE;
SingleTonEnum() {
instance = new Resource();
}
private Resource instance;
public Resource getInstance() {
return instance;
}
}
class Resource{
}
在java中枚举可以有自己的方法和属性,枚举也是绝对线程安全的,任何情况下都是一个单例。
SingleTonEnum.INSTANCE;
|