Java中的单例模式看似是一个很简单的设计模式,但事实上,我们可以整出各种各样的“幺蛾子”。单例模式有着不同的实现方式,也很难找到完美的方式。今天我就来分享一下,单例模式的几种常用实现模式以及存在的问题。
之前我写过文章讲解单例模式,不过那个是最简单的方式,还漏掉了许多的情况,这里我们就来详细地学习学习,这里还是以“一个店只能有一个老板”为例,创建老板类单例。
1,常规实现方式
(1) 饿汉式
这个就是上一篇博客讲的方法,也是最简单的实现方法:
packagecom.example.singleinstance.eager; importlombok.Getter; importlombok.Setter; /*** 饿汉式单例模式*/publicclassMaster { /*** 名字*/privateStringname; /*** 唯一单例*/privatestaticMasterinstance=newMaster(); /*** 私有化构造器*/privateMaster() { } /*** 获取老板唯一单例** @return 老板唯一单例*/publicstaticMastergetInstance() { returninstance; } }
可见饿汉式单例模式之所以叫饿汉式,是因为这种单例模式在类加载的时候就初始化了唯一单例了。
这种方式的优缺点也很明显:
- 优点:执行效率高,绝对线程安全
- 缺点:有可能用不着该单例,但是它无论如何都初始化了,可能会“占着茅坑不拉屎”,浪费内存
那么如果要改善性能,我们需要进行一些修改。
(2) 懒汉式
懒汉式单例模式就是当外部访问该单例的时候,才会初始化:
packagecom.example.singleinstance.lazy; importlombok.Getter; importlombok.Setter; /*** 懒汉式单例模式*/publicclassMaster { /*** 名字*/privateStringname; /*** 唯一单例,先不初始化*/privatestaticMasterinstance=null; /*** 私有化构造器*/privateMaster() { } /*** 获取老板唯一单例** @return 老板唯一单例*/publicstaticMastergetInstance() { // 若没有初始化,则初始化一下if (instance==null) { instance=newMaster(); } returninstance; } }
可见我们先不初始化单例,在要调用的时候,判断是否为null
,如果是说明是第一次调用,则初始化一下,否则就返回单例。
2,想办法破解单例模式
(1) 多线程破坏单例模式
懒汉式单例模式确实优化了性能,但是并非是线程安全的。假设有n个线程在极短的时间同时访问该单例的getInstance
方法,那有可能会有多余一个线程同时判断该单例为null
导致最后初始化出多个Master
实例。
我们实例化单例的时候就打印输出一下单例的地址,修改getInstance
如下:
publicstaticMastergetInstance() { // 若没有初始化,则初始化一下if (instance==null) { instance=newMaster(); System.out.println(instance); } returninstance; }
然后新建两个线程,利用IDEA的线程调试模式,干预线程的执行顺序,来模拟出两个线程同时执行到的情况:
for (inti=0; i<2; i++) { newThread(() -> { Mastermaster=Master.getInstance(); }).start(); }
在这里打断点,并右键断点-线程模式:
执行调试,可以在调试控制台这里手动切换线程,控制线程运行:
使用步入按钮(F5),先让0号线程进入if
语句,到达实例化这里停下:
切换到1号线程,也让1号线程进入if
语句,到达实例化这里停下:
最后让两个线程执行完,可以看见控制台输出了两个不同的地址:
可见,懒汉式也不完全是线程安全的。
这时,我们可以给getInstance
方法上锁,实现线程安全:
publicsynchronizedstaticMastergetInstance() { // 若没有初始化,则初始化一下if (instance==null) { instance=newMaster(); } returninstance; }
这样,确实是线程安全了,但是总归是上了锁,对程序的性能会有一定的影响,那难道就没有好一点的方法了吗?
我们可以从类的初始化的角度想一下,我们可以借助内部类来解决这些问题。在Java中,内部类是延时加载的,也就是说你用它它就加载,不用就不加载,不受外部类的影响。利用内部类的这个特性,我们是否能够把单例放在内部类里面呢?我们来试一下子:
packagecom.example.singleinstance.lazy; importlombok.Getter; importlombok.Setter; /*** 懒汉式内部类法单例模式*/publicclassMaster { /*** 名字*/privateStringname; /*** 私有化构造器*/privateMaster() { } /*** 获取老板唯一单例,final使得该方法不允许被重写或者重载** @return 老板唯一单例*/publicstaticfinalMastergetInstance() { // 返回结果之前,会先加载内部类returnInnerMaster.INSTANCE; } /*** 老板类的内部类,没有用到它就不会加载*/privatestaticclassInnerMaster { privatestaticfinalMasterINSTANCE=newMaster(); } }
这种方式完美地解决了饿汉式单例模式的内存问题,和上锁的性能问题。内部类一定是会在方法调用之前初始化,并且它永远只会初始化一次(一个类无法被加载多次),因此避免了线程安全问题。
(2) 反射破坏单例模式
构造器确实被私有化了,但是利用Java的反射机制,仍然可以访问其构造器:
// 利用反射获取构造方法,并设定可访问Constructorconstructor=Master.class.getDeclaredConstructor(); constructor.setAccessible(true); Mastermaster1= (Master) constructor.newInstance(); Mastermaster2= (Master) constructor.newInstance(); System.out.println(master1==master2);
可见即使是私有化了构造器,我们仍然还是可以把它new
个两下,得到两个实例,违背了单例模式的基本原则。
解决这个问题也不难,我们在构造器里面做点功夫即可:
packagecom.example.singleinstance.lazy; importlombok.Getter; importlombok.Setter; /*** 懒汉式内部类法单例模式*/publicclassMaster { /*** 名字*/privateStringname; /*** 私有化构造器*/privateMaster() { if (InnerMaster.INSTANCE!=null) { thrownewRuntimeException("不允许创建多个实例!"); } } /*** 获取老板唯一单例,final使得该方法不允许被重写或者重载** @return 老板唯一单例*/publicstaticfinalMastergetInstance() { // 返回结果之前,会先加载内部类returnInnerMaster.INSTANCE; } /*** 老板类的内部类,没有用到它就不会加载*/privatestaticclassInnerMaster { privatestaticfinalMasterINSTANCE=newMaster(); } }
再次运行上述代码:
好了,到这里我们也更进一步地明白了:一个类被加载时,其内部类不会被加载;而这个类被使用到时,其内部类才会被加载。
这里注意加载和使用的区别。应用程序启动时,每个类都会被加载,而你调用这个类用于实例化或者调用其方法的时候,才叫使用这个类。
(3) 序列化破坏单例模式
有时候我们需要把对象序列化并在网络上传输,然后反序列化。大家都知道,反序列化的对象并非是原有的对象,这也破坏了单例模式的原则。
首先让Master
类使用Serializable
接口,然后作如下测试:
// 序列化ByteArrayOutputStreambos=newByteArrayOutputStream(); ObjectOutputStreamoos=newObjectOutputStream(bos); oos.writeObject(Master.getInstance()); // 再反序列化ByteArrayInputStreambis=newByteArrayInputStream(bos.toByteArray()); ObjectInputStreamois=newObjectInputStream(bis); Mastermaster= (Master) ois.readObject(); System.out.println(master==Master.getInstance());
可见,利用序列化法破坏了单例。
其实,我们只需要在Master
类中增加一个readResolve
方法即可:
packagecom.example.singleinstance.lazy; importlombok.Getter; importlombok.Setter; importjava.io.Serializable; /*** 懒汉式内部类法单例模式*/publicclassMasterimplementsSerializable { /*** 名字*/privateStringname; /*** 私有化构造器*/privateMaster() { if (InnerMaster.INSTANCE!=null) { thrownewRuntimeException("不允许创建多个实例!"); } } /*** 获取老板唯一单例,final使得该方法不允许被重写或者重载** @return 老板唯一单例*/publicstaticfinalMastergetInstance() { // 返回结果之前,会先加载内部类returnInnerMaster.INSTANCE; } privateObjectreadResolve() { returnInnerMaster.INSTANCE; } /*** 老板类的内部类,没有用到它就不会加载*/privatestaticclassInnerMaster { privatestaticfinalMasterINSTANCE=newMaster(); } }
再次运行:
这看起来非常神奇:为什么加这个方法就可以了呢?事实上这和ObjectInputStream
类的执行逻辑有关。大家可以去研究一下JDK源码就知道了,再次不再过多赘述。
但事实上,这种方法确实保证只返回了一个单例,但是内存中其实还是有多个单例。
当然,肯定有更好的方法。
3,注册式单例模式
顾名思义,注册式单例模式就是把实例先注册到一个地方,获取的时候根据标识符获取。
通常有下列两种方式实现。
(1) 【推荐】枚举式单例模式
利用枚举实现单例模式,也就是把单例类写成枚举类,我们修改Master
类如下:
packagecom.example.singleinstance.enumerate; importlombok.Getter; importlombok.Setter; publicenumMaster { /*** 老板类唯一单例*/INSTANCE; /*** 名字*/privateStringname; /*** 获取老板类唯一实例** @return 老板类唯一实例*/publicstaticMastergetInstance() { returnINSTANCE; } }
大家都知道:枚举类中的每一个枚举相当于就是这个枚举类的实例,并且枚举类中也可以写成员变量和方法。
那枚举类中的枚举是不是单例呢?我们来试一下子。
a. 尝试使用反射破坏
Constructorconstructor=Master.class.getDeclaredConstructor(); constructor.setAccessible(true); Mastermaster= (Master) constructor.newInstance();
结果:
可见反射机制找不到枚举类的构造器,这是因为枚举类的构造方法是protected
的:
b. 尝试使用序列化破坏
// 序列化ByteArrayOutputStreambos=newByteArrayOutputStream(); ObjectOutputStreamoos=newObjectOutputStream(bos); oos.writeObject(Master.getInstance()); // 再反序列化ByteArrayInputStreambis=newByteArrayInputStream(bos.toByteArray()); ObjectInputStreamois=newObjectInputStream(bis); Mastermaster= (Master) ois.readObject(); System.out.println(master==Master.getInstance());
结果:
这也是利用JDK的反序列化机制,也就是说枚举类型其实是通过类名和类对象找到一个唯一的对象,不会被类加载器加载多次。
这也可见:枚举值天生就是单例的,非常契合单例模式思想。
(2) 容器式单例
我们还可以使用Map
专门做一个单例容器,把实例都放进去:
packagecom.example.singleinstance; importjava.util.Map; importjava.util.concurrent.ConcurrentHashMap; /*** 单例容器*/publicclassSingleContainer { privateSingleContainer() { } // 存放所有单例的容器,键为类的全限定名,值为对应单实例privatestaticMap<String, Object>container=newConcurrentHashMap<>(); /*** 获取对应类的单实例,不存在则创建** @param className 类的全限定名* @return 单实例*/publicsynchronizedstaticObjectgetInstance(StringclassName) throwsException { if (!container.containsKey(className)) { Objectinstance=Class.forName(className).getConstructor().newInstance(); container.put(className, instance); returninstance; } returncontainer.get(className); } }
这种方式看起来也很高级,不过也会产生线程问题。
4,总结
可见单例模式看起来简单,事实上要想写一个严谨、滴水不漏的单例模式还是很难的。
日常开发,推荐使用基于内部类的懒汉式单例模式或者是枚举式单例模式。将两者示例代码拎出来如下:
基于内部类的懒汉式单例模式:
packagecom.example.singleinstance.lazy; importlombok.Getter; importlombok.Setter; /*** 懒汉式内部类法单例模式*/publicclassMaster { /*** 名字*/privateStringname; /*** 私有化构造器*/privateMaster() { if (InnerMaster.INSTANCE!=null) { thrownewRuntimeException("不允许创建多个实例!"); } } /*** 获取老板唯一单例,final使得该方法不允许被重写或者重载** @return 老板唯一单例*/publicstaticfinalMastergetInstance() { // 返回结果之前,会先加载内部类returnInnerMaster.INSTANCE; } privateObjectreadResolve() { returnInnerMaster.INSTANCE; } /*** 老板类的内部类,没有用到它就不会加载*/privatestaticclassInnerMaster { privatestaticfinalMasterINSTANCE=newMaster(); } }
枚举式单例模式:
packagecom.example.singleinstance.enumerate; importlombok.Getter; importlombok.Setter; publicenumMaster { /*** 老板类唯一单例*/INSTANCE; /*** 名字*/privateStringname; /*** 获取老板类唯一实例** @return 老板类唯一实例*/publicstaticMastergetInstance() { returnINSTANCE; } }