你以为单例模式只有饿汉式和懒汉式吗?

133 阅读4分钟

单例模式是一种设计模式,是说在单一应用程序中只允许单例的bean只拥有一个实例

饿汉模式: 是说当jvm进行类加载的时候就创建对象的实例, 无论我们是否使用了该对象该对象都会产生一个实例,因为虚拟机保证只会装载一次,从而保证这个类是线程安全的

public class SingletonDemo {

    private SingletonDemo(){

    }

    private static SingletonDemo instance = new SingletonDemo();

    public static SingletonDemo getInstance() {
        return instance;
    }

}

饿汉模式的缺点:

· 类加载时就创建了对象浪费了程序的内存,开支了不必要的性能

懒汉模式: 使用了懒加载, 所以叫做懒汉模式,当程序第一次调用getInstance()方法的时候才会创建类的实例

public class SingletonDemo2 {

    private SingletonDemo2() {

    }

    private static SingletonDemo2 instance = null;

    public static SingletonDemo2 getInstance() {
        if (instance == null) {
            instance = new SingletonDemo2();
        }
        return instance;
    }

}

· 这种写法并不是线程安全的

比如说有两个线程,线程A线程B, A线程首先获取执行权,执行if (instance == null)条件为true,然后就会进行初始化 对象初始实例化,当A线程已经初始化还没来得及写回主内存时, B线程 也在进行B执行if (instance == null)条件为true这是程序中就会出现两个实例,违背了单例的原则 采用下面的优化方式,

    public class SingletonDemo2 {

    private SingletonDemo2() {

    }

    private static SingletonDemo2 instance = null;

    public static SingletonDemo2 getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo2.class) {
                if (instance == null) {
                    instance = new SingletonDemo2();
                }
            }
        }
        return instance;
    }

}

但是上面的解决方法还是存在线程安全问题,尽管几率比较小,但是线程安全问题依然存在 问题的根本原因在于, 在类实例化的时候会执行以下三条指令:

memory = allocate() // 为对象分配内存地址 1

ctorinstance() // 初始化对象 2

instance = memory // 设置instance指向刚分配的内存 3

2 步骤和 3 步骤并没有依赖 , 根据java内存模型的标准, 只要不影响程序的执行结果, 允许编译器和处理器对其进行指令重排序, 存在这样的可能性, A线程还没有执行初始化操作先指向了堆内存中的内存地址, 这时候B线程进来了判断if (instance == null) 这时候instance 不为null, 但是A线程并没有进行初始化操作,B线程拿到的Instance就会出现问题,因此由于jvm和cpu优化程序,指令重排序的原因导致了这种解决方案并不是线程安全的,为了解决这个问题, 需要解决重排序的问题,采用以下方案禁止jvm和cpu对对象的初始化操作进行重排序:

public class SingletonDemo2 {

   private SingletonDemo2() {

   }

   private static volatile SingletonDemo2 instance = null;

   // 使用 volatile(禁止指令重排序) 和双重检测机制实现线程安全的单例bean
   public static SingletonDemo2 getInstance() {
       if (instance == null) {
           synchronized (SingletonDemo2.class) {
               if (instance == null) {
                   instance = new SingletonDemo2();
               }
           }
       }
       return instance;
   }
}

其实改动也就是在本身实例对象用volatile关键字修饰了以下, 使用 volatile(禁止指令重排序) 和双重检测机制实现线程安全的单例bean, 写volatile修饰的变量时,程序会在这个操作前后添加store指令,读的时候在这个操作前后添加load指令从而禁止jvm和cpu对其进行重排序,确定了程序指令的执行顺序解决的隐藏的重排序引起的线程安全问题.

下面是第三种方式创建单例Bean,使用枚举

利用枚举的构造方法一定会执行,并且只会执行一次,保证了在单一应用中只存在单一实例

public class SingletonDemo3 {

   private SingletonDemo3() {

   }

   public static SingletonDemo3 getInstance() {
       return Singleton.INSTANCE.getInstance();
   }


   private enum Singleton {
       INSTANCE;

       private SingletonDemo3 instance;

       // jvm 保证这个方法只会被调用一次
       Singleton() {
           instance = new SingletonDemo3();
       }

       public SingletonDemo3 getInstance(){
           return instance;
       }
   }

   public static void main(String[] args) {
       SingletonDemo3 instance = SingletonDemo3.getInstance();
       SingletonDemo3 instance1 = SingletonDemo3.getInstance();
       System.out.println(instance == instance1);

   }

}

由于枚举的特性,构造方法一定会执行,并且仅执行一次,使用这种方式创建单例的bean是最安全的 这就是单例模式的第三种方式

以上言论只代表个人想法,如果您那里说的不对还请指出来, 大家一起进步,一起学习嘛!!!!