单例模式的实现方式有很多种,下面我用五种方式进行创建
1.饿汉式
首先解释下什么是饿汉式,因为后面有个懒汉式。饿汉式就是当类加载的时候就已经实例化好了。懒汉式则相反,当需要用的时候再进行实例化。
饿汉式的单例,三个要点。
第一:构造器要私有化。这点就毫无疑问了
第二:定义一个静态变量用来接收这个单例对象。
第三:通过public方法返回该对象实例。
代码实现:
再来说说饿汉式所存在的问题,当这个类实现了序列化接口之后。会有三种方式来破坏这个单例。
第一:通过反射获取构造方法,暴力破解,进行实例化的创建。
第二:通过反序列化,进行对象的创建。这种方式并不会走构造器。
第三:通过jdk内置的对象 unsafe来进行对象创建。
前面两种都有方法解决。unsafe目前不知道。
第一种反射创建对象的代码实现及解决方法。
通过反射进行对象的创建
解决反射破坏单例的问题
第二种通过反序列化破坏单例的代码实现和解决方案
通过反序列化破坏单例
解决反序列化破坏单例。注意:方法名必须为readResolve
至于第三种,就不说了,也没啥解决方法。
2.枚举—饿汉式
通过枚举进行单例的实现,可以不用考虑被反射和反序列化破坏单例的情况。反射和反序列化在遇到枚举的时候会进行特殊的处理。
简单的代码实现:
枚举也是饿汉式,在类加载的时候就已经将对象实例化。
3.懒汉式
懒汉式,顾名思义,需要用到时才创建。
简单代码的实现。
这种最原始的懒汉式,对于单线程来说没有问题,但是在多线程情况下,可能会创建多个实例,几个线程同时通过if语句。
在多线程情况下,为了保持单例,可以在方法上加synchronized来保证线程安全。
代码优化:
但这样加锁,加在整个类上,只要线程调用该方法,如果也有其他线程拿到锁,都会阻塞在这,大大降低效率。我们目的只是在第一次争抢时,进行加锁,实例创建完就不用再被阻塞。 下面的方案会进行优化
4.DCL懒汉式(双检索)
这种方案,顾名思义,检查了两次。
代码实现:
1.先来说说双检索。相信大家也很好读懂。
可能有人有疑惑为什么锁里面要判断两次。就简单说说。
当instance还没有实例化时,这时多个线程来调用该方法时,都会进入到第一个if判断中,这时会上锁,当第一个拿到的锁的线程创建完实例后,释放锁。此时instance已经不为null了。所以当之后的线程拿到锁进入后,如果没有if判断,就又会创建新的实例,并返回。所以要加判断。加了后,就可以明确知道,如果不为空就不进行创建。
当instance实话完成后,之后如果还有现成调用方法,就不用再进入synchronized里面进行串行判断。 可以直接在外层并行的判断是否为空。大大提高了效率。
2.再来说说为什么instance要加volatile修饰
首先说说volatile的作用, volatile保证了有序性,可见性。这里主要时保证有序性。
我们首先要知道cpu执行的是一条一条的指令。而我们的java代码要编译成一行一行的指令,让cpu执行。在底层cpu执行指令时,会根据自己的优化,把没有因果关系的指令进行重排序,也就是指令重排。
我们通过反编译class文件,可以看到在**instance = new LazySingleton()**这一行代码所对应的指令是什么。
我们一次来 说明每个指令的作用。
17: new 这个指令 在创建对象,为Singleton4这个对象开辟所需要的内存空间。
20:dup。不用管。
21: invokespecial。 调用构造方法。给对象的成员变量赋值。
24:putstatic。给静态变量instance赋值。
通过以上指令可以说明,17和21这两个指令不会交换,因为21指令给成员变量赋值的前提是要先给他内存地址,如果连在哪都不知道,如何赋值。 但21和24这两个指令可能会进行指令重排。 都是给变量赋值。没有因果关系。
所以当21,24两个指令发生指令重排时,在单线程情况下没有问题,但在多线程情况下就有问题了。
我们知道双检索的方法,里面的if判断是串行执行的,但外面的if判断是可以一直有线程在判断的。当先执行24(putstatic)这条指令后,时间片用完了。这时instance就不为空了。这时另外的线程刚好外层if进行判断时,发现不为null 就会return这个instance。我们知道这时因为指令重排,导致instance中的成员变量这些并没有赋值完成,后续在使用时就会出现问题。
这时volatile的作用就来了。 当变量加了volatile时,会给volatile修饰的变量的赋值指令后面加一个屏障,作用是,不让该变量之前的赋值操作在它后面,这也就保证了有序性。当instance加了volatile修饰后,就不会让invokespecial指令排在它后面了。
注意
懒汉式会有线程安全问题。那可能有人问,饿汉式为什么没有线程安全问题。可以看看饿汉式的代码,饿汉式是 new对象的过程是赋值给静态变量的。 static 修饰的会在类加载后 ,统一放到静态代码块执行。这个方法是由jvm底层来帮我们实现线程安全的。所以 饿汉式不用考虑线程安全。
那么问题就来了,懒汉式能不能不让我们考虑线程安全问题。答案是有的。接下来第五种方案。
5.懒汉式-内部类
懒汉式之前的是线程不安全的,要手动进行代码改动来进行线程安全。那我们可不可以借鉴饿汉式线程安全的原理来实现,懒汉式线程安全的实现。当然是可以的。
简单的代码实现:
通过静态内部类来实现,线程安全。
6.了解jdk中哪些体现了单例模式
1.Runtime这个类是单例
对于这个类大家可能不太熟悉。但System.exit()这个方法,包括System.gc()这个方法 大家可能会熟悉一点,这两个方法内部就是调用Runtime对象。
再来看看Runtime这个类
很明显就是饿汉式的单例。












