单例模式全总结-懒汉、饿汉、双重校验锁、静态内部类、枚举类

  • 懒汉(线程不安全)

public class LazySingleton{
    private static LazySingleton lazySingleton = null;  
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if(lazysingleton == null){
            lazysingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
  • 饿汉(浪费内存,空间换时间)

public class Singleton {
    private static Singleton singleton=new Singleton();
    private Singleton(){}
    public static Singleton getInsatnce(){
        return singleton;
    }
}
  • 双重校验锁-懒汉-线程安全-反射攻击

public class Singleton{
    private volatile static Singleton uniqueInstance = null;  
    private Singleton(){}                              //private 防止外部可以new
    public static Singleton getUniqueInstance(){       //public static 保证外部可以调用方法 (在public后加sychronized可以,但影响效率)
        if(uniqueInstance == null){                    //某线程完成此步可能失去CPU执行权
            sychronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

所以在上面的懒汉式代码中 singleton = new Singleton()这句,这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情

            1.给singleton分配内存

            2.调用Singleton的构造函数来初始化成员变量

            3.将singleton对象指向分配的内存空间(执行到这一步,singleton才是非null的了)

            但是在JVM的及时编译器中,存在指令重拍的优化,也就是说,第二步和第三步的顺序是无法保证的而导致程序出错

 

为什么会有指令重排序

        处理器为啥要重排序?

        因为一个汇编指令也会涉及到很多步骤,每个步骤可能会用到不同的寄存器,

        CPU使用了流水线技术,也就是说,CPU有多个功能单元(如获取、解码、运算和结果),一条指令也分为多个单元,

        那么第一条指令执行还没完毕,就可以执行第二条指令,前提是这两条指令功能单元相同或类似,

        所以一般可以通过指令重排使得具有相似功能单元的指令接连执行来减少cpu流水线中断的情况。

 

new一个对象有几个步骤

            1.看class对象是否加载,如果没有就先加载class对象,2.分配内存空间,初始化实例,3.调用构造函数,4.返回地址给引用。

            而cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了。

        再用个线程A和线程B举例。

            线程A执行到new Singleton(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。      

            这时时间片结束了,切换到线程B执行,线程B调用new Singleton()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,

            就可能导致线程B使用了还没有被初始化的变量。

 

synchronized:

      1、synchronized加在非静态方法前和synchronized(this)都是锁住了这个类的对象,如果多线程访问,对象不同,就锁不住,对象固定是一个,就可锁住。

      2、synchronized(类名.class)和加在静态方法前,是锁住了代码块,不管多线程访问的时候对象是不是同一个,类对象

         能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步,缩小锁的粒度。

 

 指令重排---2和3可能调换顺序

        1. 分配空间

        2. 实例化对象

        3. 引用指向空间

 

  • 静态内部类-线程安全-懒汉式-无法传参

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

 使用静态内部类能保证线程安全的原因

    1. 由于内部静态类只会被加载一次,故该实现方式是线程安全的

    2. 类加载的初始化阶段是单线程

 

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。

        即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,

        第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

 

  类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。

        1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,

            为什么外部类加载时静态内部类未加载,《effective java》里面说静态内部类只是刚好写在了另一个类里面,

            实际上和外部类没什么附属关系。(但直接放在外部,1. 如果设置为public访问没有限制 2. private的话访问受限)

            对应的java代码场景为:

                new一个关键字或者一个实例化对象时、

                读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、

                调用一个类的静态方法时。

        2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,

                需要先调用其初始化方法进行初始化。

        3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。

        4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

        5.当使用JDK 1.7等动态语言支持时,

            如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,

            并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。这5种情况被称为是类的主动引用,注意,

            这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,

            称为被动引用。静态内部类就属于被动引用的行列。

 

  我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,

        跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,

        取的都是同一个INSTANCE对象,而不用去重新创建。

        当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,

        把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。

        那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

 

      虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,

        那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

        如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,

        但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),

        在实际应用中,这种阻塞往往是很隐蔽的。

 

    故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

 

    那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,

      由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

  • 枚举模式(单元素的枚举类型已经成为实现Singleton的最佳方法)

public enum SingleTon{
    INSTANCE;
    public void method(){
        //TODO
    }
}

SingleTon.INSTANCE;

 

枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。我们可直接以SingleTon.INSTANCE

的方式调用。

 

为什么要用枚举类实现单例

    双重检查锁存在两个问题

    1. 私有化构造器并不保险,反射攻击

    2. 序列化问题

你可能感兴趣的:(java,设计模式)