美文网首页EASY题
[Java]再次探讨Java单例

[Java]再次探讨Java单例

作者: DrunkPian0 | 来源:发表于2017-12-09 15:35 被阅读19次

    0x00

    从前在读Effective Java的时候,第一篇的Item 1:使用静态工厂方法而不是构造器
    的静态工厂方法,其实就是单例。里面列举了单例的几个优点,比如可以自己取名字,简洁,不用每次创建对象等等。最近看了小灰的公众号又探讨了单例,所以再次学习一下。

    0x01 单例的种类

    虽然实现起来可以多种多样(比如用HashMap..,其实单例就是一种缓存思想的体现),但是单例一共可以分为两类,也就是网上取的很土的名字,饿汉和懒汉。前者是空间换时间,也就是在类初始化阶段就创建对象;后者是时间换空间,在用的时候检查对象是否为空。具体实现就不贴出来了,脑补一下吧。

    0x02 线程安全

    饿汉是线程安全的。在类被初始化的时候就创建对象,外界的线程还没有开始竞争getInstance,当然也就不会重复初始化。

    既然饿汉是线程安全的,跟懒汉的性能差距到底在哪?我是这样想的
    比如两个线程同时调用一个类的getInstance方法,这个类还没有被加载的话,是要经历类的加载的一系列流程(编译,链接,准备,初始化等)加载到内存中去的,静态的instance会保存在Java虚拟机的方法区(Method Area)作为永久代,永久代是基本不会涉及GC的,持续到进程结束都会有这个静态变量存在

    懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,费判断的时间,当然,如果一直没有人使用的话,那就不会创建实例,节约内存空间。

    懒汉式不是线程安全的,因为两个线程可能同时走到if(mInstance == null)这个语句,会导致重复创建instance。

    0x03 如何保证懒汉式单例的线程安全

    - APPROACH 1: 给整个getInstance方法加上同步锁,synchronized关键字

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

    缺点是,同步是需要开销的,因为多数情况,instance是不为null的,只要不是第一次初始化,都只有最后一行 return instance会被执行,每次都同步太浪费了。

    - APPROACH 2: DCL(Double Checking Lock)双重检测加锁

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

    如果instance还没有初始化,会进入第一重if的body里面,这时候就要同步了,防止重复创建实例。

    第二个if的作用是,如果后执行的线程在synchronized语句那里等着了,先执行的线程去创建对象了,这时候对象就不为空了,那后进来的线程就没必要再创建一次对象了。

    为什么要使用volatile 修饰instance?
    主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
    1.给 instance 分配内存
    2.调用 Singleton 的构造函数来初始化成员变量
    3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。
    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

    要格外注意的是volatile关键字。如果不加volatile,编译器在编译阶段会把内层的if语句去掉(这样一来synchronized也不起作用了),因为编译器是不负责线程安全的。但是加了volatile等于告诉编译器,instance是随时可变的,编译的时候就不会把内层if去掉了。

    Volatile的两个语义

    volatile是JVM提供的最轻量级的同步方式。
    当一个变量定义成volatile之后,它将具备两种特性:
    1. 第一是保证此变量对所有线程的立即可见性。
    这里突出一个立即可见,修改对所有线程是立即可见的。
    这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其它线程是可以立即得知的!
    普通变量做不到这一点。普通变量的变量值在线程间传递,需要通过主内存来完成,如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量的值才会对线程B可见。

    但是,使用volatile并不代表线程安全,深入理解Java虚拟机的书上举了例子,起20个线程,对一个static变量int race进行自增,每个线程执行1000次,最后发现每次执行结果都不一样,但都小于预期的20000次。这是因为,race++在字节码会分为4条指令来执行:


    race++对应的4条指令

    所以当getstatic取race的时候,volatile保证它是正确的,但是执行iconst_1和iadd的时候,其他线程可能已经执行完了putstatic,这时候再putstatic,就会把较小的值同步回主内存去。

    由于volatile变量只能保证可见性,在不符合以下条件规则的去处场景中,仍然需要通过加锁来保证原子性。
    1.运算结果不依赖变量的当前值,或者能确保只有单一的线程改变变量的值。
    2.变量不需要与其它的状态变量共同参与不变约束。

    2. 第二个语义是禁止指令重排优化(或说,保证原子性)。
    指令重排是指CPU允许将指令不按程序规定的顺序分开发送给相应的电路单元处理。比如A + 10然后再乘以2,B+3,那么B的操作与A是无关的,可以插入到A+10后面处理。
    volatile保证了指令的顺序执行。
    下面一段是抄的(我发先这段被很多地方抄了,比如程序员小灰..):

    指令重排序是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。

    下面两段也是抄的,把double/long/reference的原子性和指令重排解释得非常好:

    原子性针对一个long或者double或者一个引用类型,对于引用类型,原子性是指在new实例的过程中,不会被其他线程取到,即不会被其他线程取到一个不完整的实例。这种原子性可以理解为new的过程处于一个synchronize段的set方法中,只有set结束才可以被get到,即new的整个过程都是处于set中的。也可以理解为指令重排序,禁止把new过程的指令与把引用赋值给变量的语句重排序,赋值只发生在new结束之后。

    long和double型变量,通常需要分两步读写一个double变量,volatile修饰的double可以保证对一个double变量的操作的两部分不会被多线程插入。以及对引用类型赋值的,new一个实例的过程不会被其他线程插入(new在编译指令中是分成几步执行的,防止这几步在执行过程中被其他线程取这个变量值,取到一个不完整的实例)

    例如 instance = new Singleton() 可分解为如下伪代码:

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

    指令重排:

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

    将第2步和第3步调换顺序,在单线程情况下不会影响程序执行的结果,但是在多线程情况下就不一样了。线程A执行了instance = memory(这对另一个线程B来说是可见的),此时线程B执行外层 if (instance == null),发现instance不为空,随即返回,但是得到的却是未被完全初始化的实例,在使用的时候必定会有风险,这正是双重检查锁定的问题所在(不使用volatile的情况下)!

    0x05 静态内部类实现单例(懒加载)

    除了懒汉和饿汉,这是第三种单例,是前面两种的结合。

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

    我这里把instance写成final的,我也看到有些blog上没把它写成final,查了下,是因为类的加载过程是线程安全的。
    加载类的时候,静态内部类不会被加载。静态内部类不持有外部类的引用。这相当于懒汉,不会占用内存。调用getInstance的时候,会加载静态内部类,同时初始化实例。这相当于饿汉,也是线程安全的。相当于用静态内部类替代synchronized。

    总结,上面讲了三种线程安全的单例:

    1. 提前初始化(饿汉)。
    2. 双重检查锁定 + volatile。
    3. 延迟初始化占位类模式。

    0x06 反射打破单例

    单例的前提是构造函数是private的,这样外面才不能new,只能get。但是如果用反射,可以获取构造器。
    利用反射打破单例:

    //获得构造器
    Constructor con = Singleton.class.getDeclaredConstructor();
    //设置为可访问
    con.setAccessible(true);
    //构造两个不同的对象
    Singleton singleton1 = (Singleton)con.newInstance();
    Singleton singleton2 = (Singleton)con.newInstance();
    //验证是否是不同对象
    System.out.println(singleton1.equals(singleton2));
    

    最后的equals判断,结果肯定是false了。

    解决方法是使用Enum。

    public enum SingletonEnum {
        INSTANCE;
    }
    

    (这部分程序员小灰写的好像不太对,他忘了把私有构造方法写进去了。他想表达的就是,普通class的构造方法,即便写成private,也是可以反射得到的,而Enum的构造器,在反射的时候会报错。)
    Enum语法糖,JVM会阻止反射获取枚举类的私有构造方法。
    缺点是枚举类不是懒加载的(这一点我不太懂)。

    Ref:
    http://blog.csdn.net/zhangzeyuaaa/article/details/42673245
    http://blog.sina.com.cn/s/blog_6b6468720100kpif.html
    https://www.cnblogs.com/guangshan/p/4977542.html(原子性)
    http://blog.csdn.net/bjweimengshu/article/details/78706873

    相关文章

      网友评论

        本文标题:[Java]再次探讨Java单例

        本文链接:https://www.haomeiwen.com/subject/inyuixtx.html