Java 单例模式的十种实现方式

本文介绍Java中单例模式实现的多种方式以及各自特点。包括:

1、单例模式概述

2、单例模式实现的基本思路

3、单例模式示例代码以及分析

3.1、饿汉式(静态常量)[可用]

3.2、懒汉式(线程不安全)[不可用]

3.3、懒汉式(线程安全)[不推荐用]

3.4、懒汉式(线程不安全)[不可用]

3.5、未使用volatile的双重检查(线程不安全)[不可用]

3.6、使用volatile的双重检查(线程安全)[可用]

3.7、静态内部类[推荐]

3.8、饿汉式(静态代码块)[可用]

3.9、枚举方式[推荐](饿汉式和懒汉式两种)

3.10、序列化和反序列化下单例模式的分析

---正文---

1、单例模式概述

单例模式是一种常用的软件设计模式,也是著名的GoF23种设计模式之一,是指是单例对象的类只能允许一个实例存在。单例模式在多线程情况下保证实例唯一性的解决方案。

2、单例模式实现的基本思路

单例模式从初始化类的早晚分饿汉式和懒汉式两种实现方式。

饿汉式会在使用类前就先初始化好该单例类。

懒汉式则会在使用时才初始化该单例类,可以避免提前初始化和不必要的初始化。

基本思路如下:

<1、将该类的构造方法定义为私有方法,目的是防止其他代码通过调用该类的构造方法来实例化该类的对象,以到达只有通过该类提供的静态方法来得到该类唯一实例的效果;

<2、在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

3、单例模式示例代码以及分析

3.1、饿汉式(静态常量)[可用]

实现代码如下面图1所示:

图1

测试代码如下面图2所示:

图2

结果如下面图3所示:

分析:

从实现代码中可以看出,单例类使用了final修饰,防止被继承。下文中的单例实现也都使用了final进行修饰类。

优点:这种写法比较简单,就是在类装载的时候就完成实例化。singleton作为类变量,在类初始化过程中,会被收集进<clinit>()方法中,该方法会保障同步,从而避免了线程同步问题。

缺点:在类装载的时候就完成实例化,若从未使用过这个实例,则会造成内存的浪费。

3.2、懒汉式(线程不安全)[不可用]

实现代码如下面图4所示:

图4

测试代码和上文3.1图相同,这里不再展示。

结果如下面图5所示:

分析:

在实现代码中的构造方法中,睡眠2秒来模拟对象创建前的一些准备工作。

从上面程序执行结果可知,在多线程下,此方式并不能保证单例类的实例的唯一性。多线程执行情况如下面图6所示:

图6

从上面图6可知,如果两个线程同时看到instance为null,则getInstance()方法将无法保证单例类的实例化对象的唯一性。

注:测试结果也会出现对象的hashcode一致的现象,这是因为多个线程不总是同时到达instance == null处,不过如果在构造方法中没有模拟一些对象创建前的准备工作,那么会大大增加多个线程同时到达instance == null的几率。而且在现实情况下,单例类总是会做一些初始化前的准备工作。

注:此方式在确定是单线程的情况下,可以保证创建的对象唯一性,大家可以将测试类改为一个线程尝试下。

3.3、懒汉式(线程安全)[不推荐用]

实现代码如下面图7所示:

图7

测试代码和上文3.1图相同,这里不再展示。

结果如下面图8所示:

图8

分析:

可以看到,因为使用了锁,同一时间只能有一个线程进入getInstance()方法,方法中实例化单例类的代码仅第一个获得synchronized锁的线程会执行一次,后面其他线程想获得该类实例,会直接获取已有的实例化对象,所以保证了单例类的实例化对象的唯一性。但也正是如此,执行getInstance()方法会因同步而发生阻塞,三个线程在几乎同一时间完成了调用getInstance()方法的调用。方法进行同步效率太低有待改进。

3.4、懒汉式(线程不安全)[不可用]

看过我之前写的synchronized的各种骚操作骚要点-对象锁、类锁、this锁、非this锁等这篇文章的话,应该会问使用同步代码块是否可以呢?答案是sorry,不可以。

实现代码如下面图9所示:

图9

测试代码和上文3.1图相同,这里不再展示。

结果如下面图10所示:

图10

分析:

仅在使用共享变量处加锁,确实能提高效率,但为什么不行想必大家已经猜到:如果两个线程同时执行到 if (singleton == null)时,则两个线程便都进行实例化从而得到两个不同的对象。

注:上面实现代码需要执行多次才会有可能出现hashcode不一致的情况,或需要模拟创建对象前的一些准备工作(即sleep一会)才会出现异常情况。

3.5、未使用volatile的双重检查(线程不安全)[不可用]

实现代码如下面图11、图12、图13所示:

图11

图12

图13

测试代码如下面图14所示:

图14

结果如下面图15所示:

图15

分析:

未使用volatile的双重检查看似提供了一种高效的同步策略,即首次初始化时加锁,之后允许多线程同时进行getInstance()方法的调用来获取单例类的实例。

但这种方式下可能引起空指针异常,原因是在Singleton的构造函数里,分别实例化了TempObject1和TempObject2两个类,还有Singleton自己,但由于JVM运行时指令重排序和Happens-Before规则,这三者的实例化顺序并无先后关系的约束,那么极有可能是Singleton先被实例化,而TempObject1和TempObject2两个类并未完成初始化就被其他线程调用了本类方法而出现空指针异常。如下面图16所示:

图16

注:结果图中,并未出现空指针异常,应该与我自己机器有关。

3.6、使用volatile的双重检查(线程安全)[可用]

仅需将未使用volatile的双重检查的实现代码中private static Singleton singleton;改为 private volatile static Singleton singleton;即可。原理是volatile保证了singleton的可见性以及抑制重排序,且volatile 不会阻塞。详情参见Java volatile 从入门到精通。

示例图略。

3.7、静态内部类[推荐]

实现代码如下面图17所示:

图17

测试代码和上文3.1图相同,这里不再展示。

结果如下面图18所示:

分析:

使用静态内部类的方式实现单例模式,是利用了类加载的特点。将Singleton类型变量singleton放在静态内部类SingletonInstance中,因此Singleton类在初始化过程中并不会创建Singleton实例,而SingletonInstance类中定义了Singleton类型的静态变量并直接实例化,当SingletonInstance中的Singleton类型变量singleton被引用时,会直接创建单例类的实例,又因singleton为静态变量,故创建时是在<clinit>()方法中。上文说过<clinit>()方法的同步特性,利用该特性保证了单例类的实例唯一性。

静态内部类方式是最好也是最常用的几种实现单例模式的方式之一。

3.8、饿汉式(静态代码块)[可用]

实现代码如下面图19所示:

图19

测试代码和上文3.1图相同,这里不再展示。

结果如下面图20所示:

分析:

关于静态代码块:

它是随着类的加载而执行,只执行一次,并优先于主函数。具体说,静态代码块是由类调用的。类调用时,先执行静态代码块,然后才执行主函数的。静态代码块其实就是给类初始化的,而构造代码块是给对象初始化的。静态代码块中的变量是局部变量,与普通函数中的局部变量性质没有区别。一个类中可以有多个静态代码块。

注:关于类加载的详细内容,我会在虚拟机的总结中进行梳理,不过虚拟机部分我暂时更新到了java虚拟机(第二版) 第二章总结 (三)-手工复现java虚拟机内存溢出(OutOfMemoryError异常),还得一段时间,着急的话可以购买 java虚拟机(第二版) 仔细研读。

3.9、枚举方式[推荐]

3.9.1、饿汉式枚举实现单例方式

实现代码如下面图21所示:

图21

测试代码如下面图22所示:

图22

结果如下面图23所示:

分析:

我们在测试方法中仅Singleton.getInstance(),可以看到结果和我们预期一致,下面我们放开注释掉的测试方法Singleton.method();看看结果,如下面图24所示:

图24

可以看到,尽管method()方法和getInstance()方法都被调用了,但单例类的构造方法只被初始化一次,说明仅构造了一次单例类。下面我们将测试代码中的Singleton.getInstance()注释掉,结果如下面图25所示:

图25

可以看出,构造方法还是仅被调用一次,也就是仅实例化了一个对象。也验证了 调用method()方法会主动使用Singleton,INSTANCE被实例化 。

注:使用枚举的方式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

3.9.2、懒汉式枚举实现单例方式

实现代码如下面图26:

图26

测试代码和上文3.1图相同,这里不再展示。

结果如下面图27所示:

分析:

除了实现了懒加载特性外,其他特性特性参见3.9.1、饿汉式枚举实现单例方式的分析。

3.10、序列化和反序列化下单例模式的分析

实现代码如下面图28所示:

图28

测试代码如下面图29所示:

图29

结果如下面图30所示:

分析:

从结果可知,明明是将一个对象写文件,然后又将文件中的对象读出,按照预期singleton和singleton2应该是是同一个对象,但从测试结果可知,并不是像我们预期的那样。如果想实现写入文件和从文件中读取的对象为同一个,只需放开单例类的readResolve()方法即可。

放开单例类的readResolve()方法后的测试结果如下面图31所示:

可以看到放开readResolve()方法后序列化和反序列化下单例类依然是同一个对象。

本篇完。

猜你喜欢

转载自blog.csdn.net/weixin_39214481/article/details/88356685