设计模式之单例模式

一、概念

JVM中,单例对象只有一个实例存在。

二、饿汉式实现

public class Singleton {
    private static Singleton instance = new Singleton();
        
    private Singleton() {
    }
     
    public static Singleton getInstance() {
        return instance;
    }
}

最简单的实现方式,但是如果对象的构造耗费时间,可能采用懒汉式更好。

三、懒汉式实现一

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

也是很简单粗暴的懒汉式实现方式,每次获取单例的时候都需要获取排他锁,效率差。

四、懒汉式实现二

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;
    }
}

1、双重判断

  • 第一次判断是防止实例化完毕后的无效同步处理
  • 如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

2、指令重排

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。
也就是说,JVM为了执行效率会将指令进行重新排序,但是这种重新排序不会对单线程程序产生影响。

由于instance = new Singleton();操作并不是一个原子性指令,会被分为多个指令:

memory = allocate();  //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory;    //3:设置instance指向刚分配的内存地址

但是经过重排序后如下:

memory = allocate();  //1:分配对象的内存空间
instance = memory;    //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); //2:初始化对象

3、可见性

instance需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间(空白内存)并将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

五、懒汉式实现三

public class Singleton {
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();

        private SingletonHolder(){
        }
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

1、加载时机

内部静态类是要在有引用了以后才会装载到内存的,所以在你第一次调用getInstance()之前,SingletonHolder是没有被装载进来的,只有在你第一次调用了getInstance()之后,里面涉及到了return SingletonHolder.instance; 产生了对SingletonHolder的引用,内部静态类的实例才会真正装载。这也就是懒加载的意思。

遇到new、getstatic、putstatic、或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2、线程安全

虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,只到活动线程执行()方法完毕。

posted @ 2020-07-23 23:32  Mr靖哥哥  阅读(129)  评论(0编辑  收藏  举报