5种单例模式的实现

167 阅读6分钟

单例模式的实现方式有很多种,下面我用五种方式进行创建

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这个类

很明显就是饿汉式的单例。