![设计模式就该这样学:基于经典框架源码和真实业务场景](https://wfqqreader-1252317822.image.myqcloud.com/cover/758/33114758/b_33114758.jpg)
8.2 使用单例模式解决实际问题
8.2.1 饿汉式单例写法的弊端
其实我们前面看到的单例模式通用写法,就是饿汉式单例的标准写法。饿汉式单例写法在类加载的时候立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现之前就实例化了,不可能存在访问安全问题。饿汉式单例还有另外一种写法,代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_3.jpg?sign=1738887747-kSP5bnau5x63Nn9qQqZekdRdNG6b1UKz-0-329e506a8123cf4869e8c4a3954bcb0c)
这种写法使用静态块的机制,非常简单也容易理解。饿汉式单例写法适用于单例对象较少的情况。这样写可以保证绝对线程安全,执行效率比较高。但是它的缺点也很明显,就是所有对象类在加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,而且单例对象的数量也不确定,则系统初始化时会造成大量的内存浪费,从而导致系统内存不可控。也就是说,不管对象用或不用,都占着空间,浪费了内存,有可能占着内存又不使用。那有没有更优的写法呢?我们继续分析。
8.2.2 还原线程破坏单例的事故现场
为了解决饿汉式单例写法可能带来的内存浪费问题,于是出现了懒汉式单例的写法。懒汉式单例写法的特点是单例对象在被使用时才会初始化。懒汉式单例写法的简单实现LazySimpleSingleton如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_4.jpg?sign=1738887747-xuovQmLV9i72qDCDBs49GvGe7R5oC7Xo-0-287c895263f4726c2f9bfe809173f6fd)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_5.jpg?sign=1738887747-XSb1niWipqmu4nObgCdutCNCeannR7vT-0-c18e15844419fa7b0316aa43adf5de1c)
但这样写又带来了一个新的问题,如果在多线程环境下,则会出现线程安全问题。先来模拟一下,编写线程类ExectorThread。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_6.jpg?sign=1738887747-UE3YlPysb7oqBmnJwI5gHRiDmAj89Vd8-0-0f10d8e582b98100e35bfc5203ac7069)
编写客户端测试代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_7.jpg?sign=1738887747-wOiEeQAac6XigXEZlTTQEYEoaVC7oFH5-0-bb2b6bb545ce5c02699a3c009eb71e2f)
我们反复多次运行程序上的代码,发现会有一定概率出现两种不同结果,有可能两个线程获取的对象是一致的,也有可能两个线程获取的对象是不一致的。下图是两个线程获取的对象不一致的运行结果。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_8.jpg?sign=1738887747-3MKrxl2YKJ4z3gW7ia06jRorAqZkoNmo-0-a5936cd525a436e2358d2d5bc30e442c)
下图是两个线程获取的对象一致的结果。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_9.jpg?sign=1738887747-0XUvzxM4Fv3DB2EnWE1Gmg7ksOpTcrzy-0-5c311649ddfb34a40c605cf0ee299263)
显然,这意味着上面的单例模式存在线程安全隐患。那么这个结果是怎么产生的呢?我们来分析一下,如下图所示,如果两个线程在同一时间同时进入getInstance()方法,则会同时满足if(null== instance)条件,创建两个对象。如果两个线程都继续往下执行后面的代码,则有可能后执行的线程的结果覆盖先执行的线程的结果。如果打印动作发生在覆盖之前,则最终得到的结果就是一致的;如果打印动作发生在覆盖之后,则得到两个不一样的结果。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_10.jpg?sign=1738887747-2D17JR0ubf2t99yHQ1AM19zgH7kv4yE9-0-968d94337810fd7c7bb30894f2f2401b)
当然,也有可能没有发生并发,完全正常运行。下面通过调试方式来更深刻地理解一下。这里教大家一种新技能,用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化。先把ExectorThread类打上断点,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_11.jpg?sign=1738887747-WIaeopg62tKFAjz6dMFsJ25SqZ97Jtw9-0-2a4f4a2dbc83ab658e343261a763b74d)
单击右键点击断点,切换为Thread模式,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_12.jpg?sign=1738887747-s3oqA9ZlXjKllFM5LOe8BOFfsisCi24a-0-070625397e2f591b24aef97fddc6bb05)
然后把LazySimpleSingleton类也打上断点,同样标记为Thread模式,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_13.jpg?sign=1738887747-K30mG1lQac9o3TOLFVzG1kOtYd6h9GSR-0-dc3c8c1ce4969b8d33cff1de222ac80a)
切换回客户端测试代码,同样也打上断点,同时改为Thread模式,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_14.jpg?sign=1738887747-ZJck9rcoZzBD53ujeRDdS84XbNZbRctu-0-1e265439687f1c856be7129ef0bacd8c)
在开始Debug之后,我们会看到Debug控制台可以自由切换Thread的运行状态,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_15.jpg?sign=1738887747-7l4Ph2Zi9L7iBMjgJBWrUAFQZolkizjr-0-ddb5004dbc0dbc7b4392b8d539239753)
通过不断切换线程,并观察其内存状态,我们发现在线程环境下LazySimpleSingleton被实例化了两次。有时候得到的运行结果可能是两个相同的对象,实际上是被后面执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,如何优化代码,使得懒汉式单例写法在线程环境下安全呢?来看下面的代码,给getInstance()方法加上synchronized关键字,使这个方法变成线程同步方法。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_16.jpg?sign=1738887747-aq77fY6dvuP4LBhNwAg968bmKUJxs3sX-0-2d978e7a24179cada2e382e76184381d)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_17.jpg?sign=1738887747-YKuxV53VUeZfS3eeGlsPagiZHe9DRxrs-0-87599be76677339ce273be6c3a3096b0)
我们再来调试。当执行其中一个线程并调用getInstance()方法时,另一个线程在调用getInstance()方法,线程的状态由RUNNING变成了MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到RUNNING状态继续调用getInstance()方法,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_18.jpg?sign=1738887747-6YHhbSObW6NCuE687UDilpF2t11ZTnDO-0-b95f04a67542a6d5e05d434eb7d8738a)
这样,通过使用synchronized就解决了线程安全问题。
8.2.3 双重检查锁单例写法闪亮登场
在上一节中,我们通过调试的方式完美地展现了synchronized监视锁的运行状态。但是,如果在线程数量剧增的情况下,用synchronized加锁,则会导致大批线程阻塞,从而导致程序性能大幅下降。就好比是地铁进站限流,在寒风刺骨的冬天,所有人都在站前广场转圈圈,用户体验很不好,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_19.jpg?sign=1738887747-PU9TCgSJUPywwgi285PvQo8IeftMQePf-0-ac824b1f98c4f28a154d3a23764a8692)
那有没有办法优化一下用户体验呢?其实可以让所有人先进入进站大厅,然后增设一些进站闸口,这样用户体验变好了,进站效率也提高了。当然,在现实生活中可能会受到很多硬性条件的限制,但是在虚拟世界中是完全可以实现的。其实这就叫作双重检查,在进站门安检一次,进入大厅后在闸口检票处再检查一次,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_20.jpg?sign=1738887747-4jSx36TBljA0CnUcv88eWYJvKWVjwduh-0-f16117907fa64c662722219c9e669834)
我们来改造一下代码,创建LazyDoubleCheckSingleton类。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_21.jpg?sign=1738887747-oR4Efhd53r0j3QOj9ow3NHo5u1bnwvTa-0-59a09f4afd31d3746aaef4cc282f57c2)
这样写就解决问题了吗?目测发现,其实这跟LazySimpleSingletion的写法并无差异,还是会大规模阻塞。那我们把判断条件往上提一级呢?
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_22.jpg?sign=1738887747-abXysuTlUWa6LkoAvhRjBZ926Yu1JySa-0-3e5a091464c66b272c308077f17ab810)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_23.jpg?sign=1738887747-j8hshiNYEPqlCyMGv4wNCTPo5yyT0IEn-0-f2c64912b2fc19ed8ca91d8808d9a377)
在运行代码后,还是会存在线程安全问题。运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_24.jpg?sign=1738887747-yF6GotIBev2tAWB3Tovp3DXxRSe1gjfY-0-a4bcb3258aa4591cd6375425007d308a)
这是什么原因导致的呢?其实如果两个线程在同一时间都满足if(instance == null)条件,则两个线程都会执行synchronized块中的代码,因此,还是会创建两次。再优化一下代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_25.jpg?sign=1738887747-z7FVTLo7QMRzVNwykNgr48X6qmC67q4G-0-efe64a9b9fb081fc2a6434e80b07b8a8)
我们进行断点调试,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_26.jpg?sign=1738887747-HW8QKdDddpyXeRxtUFciGIkeQQURGHVS-0-19252cdb601a390608cb29bd066b9f74)
当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazyDoubleCheckSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感觉不到。
8.2.4 看似完美的静态内部类单例写法
双重检查锁单例写法虽然解决了线程安全问题和性能问题,但是只要用到synchronized关键字就总是要上锁,对程序性能还是存在一定影响的。难道真的没有更好的方案吗?当然有。我们可以从类初始化的角度考虑,看下面的代码,采用静态内部类的方式。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_27.jpg?sign=1738887747-m5fRbegvAH8CiIwaQf6q3uLuPJWg1H8a-0-fbcca4c62db4246516ef01e65bca4277)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_28.jpg?sign=1738887747-6zWtaND6UoX7V21j2z8eah1bR21aPvQj-0-c1fde2645b7bb0054666545cbb81260c)
这种方式兼顾了饿汉式单例写法的内存浪费问题和synchronized的性能问题。内部类一定要在方法调用之前被初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,就不再一步步调试。但是,“金无足赤,人无完人”,单例模式亦如此。这种写法就真的完美了吗?
8.2.5 还原反射破坏单例模式的事故现场
我们来看一个事故现场。大家有没有发现,上面介绍的单例模式的构造方法除了加上private关键字,没有做任何处理。如果使用反射来调用其构造方法,再调用getInstance()方法,应该有两个不同的实例。现在来看客户端测试代码,以LazyStaticInnerClassSingleton为例。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_29.jpg?sign=1738887747-M8vzSIfdXrF6wXuasy6YkhI0D089ye9k-0-1eedd7ca1fd66a1f643a461219d2d03f)
运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_30.jpg?sign=1738887747-lW2m37v93MEWwvHWCmwZ3D6kh19L371r-0-45b6e67866d9338a3b32fa6efb786c75)
显然,内存中创建了两个不同的实例。那怎么办呢?我们来做一次优化。我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。优化后的代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_31.jpg?sign=1738887747-fNSLnL1lJHbXGrfuHWVYZ8CfBoucCF0u-0-2facae578def6c1b2682730b8d3bd9b8)
再运行客户端测试代码,结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_32.jpg?sign=1738887747-1TDReTI0cxWRAPhft6dgJbAR4w3tcAIj-0-e14566d30a5869ee052299ed5760dcde)
至此,自认为最优雅的单例模式写法便大功告成了。但是,上面看似完美的单例写法还是值得斟酌的。在构造方法中抛出异常,显然不够优雅。那么有没有比静态内部类更优雅的单例写法呢?
8.2.6 更加优雅的枚举式单例写法问世
枚举式单例写法可以解决上面的问题。首先来看枚举式单例的标准写法,创建EnumSingleton类。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_33.jpg?sign=1738887747-jMUF7Md3kFNgxZ8fpW53pZoToFIqPDJz-0-56981e896a2b1760be6c35c0fc8524d4)
然后看客户端测试代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_34.jpg?sign=1738887747-XoyLlQJE8paG9VxgMNISKUFYhWYRhgJb-0-c1f6fc40b3ac913336362282fcfa6603)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_35.jpg?sign=1738887747-fws37BHm0tEWEQQ61MqXugalAW7PBVrJ-0-43ede6f845482a96b669437e2f27788e)
最后得到运行结果,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_36.jpg?sign=1738887747-rvGbRwD7WvYli3onu4rCyTzeXvQRDAH9-0-4c250a2520934d7148364d09080fd07a)
我们没有对代码逻辑做任何处理,但运行结果和预期一样。那么枚举式单例写法如此神奇,它的神秘之处体现在哪里呢?下面通过分析源码来揭开它的神秘面纱。
首先下载一个非常好用的Java反编译工具Jad,在解压后配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制EnumSingleton.class所在的路径,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_37.jpg?sign=1738887747-PiEWGjxjqrurAtHfpzvuptf6pVmxOpmP-0-b12f6524b0094d62592ff330b7719a45)
然后切换到命令行,切换到工程所在的Class目录,输入命令jad并输入复制好的路径,在Class目录下会多出一个EnumSingleton.jad文件。打开EnumSingleton.jad文件,我们惊奇地发现有如下代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_38.jpg?sign=1738887747-ooWBKmX0T4DyFmo6KsXNizJd65sSRBb8-0-81d146b05f68e89a24815c904080222b)
原来,枚举式单例写法在静态块中就对INSTANCE进行了赋值,是饿汉式单例写法的实现。至此,我们还可以试想,序列化能否破坏枚举式单例写法呢?不妨再来看一下JDK源码,还是回到ObjectInputStream的readObject0()方法。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_39.jpg?sign=1738887747-fw1sFQ6vApGfhMUVYFJA9qKgcmJ1X2CS-0-3b4d2accfca882251c37dc48e9c7bdf8)
我们看到,在readObject0()中调用了readEnum()方法,readEnum()方法的代码实现如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_40.jpg?sign=1738887747-Ch47YJy31Bh8B3QxxXW2OFZtBBYJOlRN-0-f0e4d135be8259b4dcf1745c4698789f)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_41.jpg?sign=1738887747-sqmV2dlD2SL4OAfn5iqv7Dv3HTApTT7Z-0-93926528c4eb3f8eb60b0d1065f41a6c)
由上可知,枚举类型其实通过类名和类对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。那么反射是否能破坏枚举式单例写法的单例对象呢?来看客户端测试代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_42.jpg?sign=1738887747-MiYIyDBilTMH2nxzVHZahDDQhvxR77ij-0-a9b1c3b6cb9b76dc592839b233f0c546)
运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_43.jpg?sign=1738887747-N0OkuzKJAot5ZxlhQZaczrlGl94y8zCc-0-db7e8bd330639bd2c5aaa57415271530)
结果中报出的是java.lang.NoSuchMethodException异常,意思是没找到无参的构造方法。此时,打开java.lang.Enum的源码,查看它的构造方法,只有一个protected类型的构造方法,代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_44.jpg?sign=1738887747-5mjtv4iApFM9MVbJ7IicNyi7v3fQDv2W-0-ad98cf874348451bf9dfb7de473bf1f8)
再来做一个这样的测试。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_45.jpg?sign=1738887747-SCoZjEdNQsk2I5MK4DmNzYIqCJmVi4RZ-0-c4531c8ed864ede6370225b3774ce4d0)
运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_46.jpg?sign=1738887747-FFaGoTlX9Zupu6ELSQM8Fo1PMqibULfe-0-4b4f6f6a5a29b53034ebf871bffbebb7)
这时,错误已经非常明显了,“Cannot reflectively create enum objects”,即不能用反射来创建枚举类型。我们还是习惯性地想来看下JDK源码,进入Constructor的newInstance()方法。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_47.jpg?sign=1738887747-UscpGLhaFaXq294ULRIlHAZt9Z7tYGMi-0-4c3bbd11d540ee1c54a2a750e106258c)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_48.jpg?sign=1738887747-CLTv4TiYptkd4NZJ8WbnQaOfWS30s6Yi-0-9776e5bb4a4529ae9283ec9bee20e907)
从上述代码可以看到,在newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM枚举类型,则直接抛出异常。这岂不是和静态内部类单例写法的处理方式有异曲同工之妙?对,但是我们在构造方法中写逻辑处理可能存在未知的风险,而JDK的处理是最官方、最权威、最稳定的。因此,枚举式单例写法也是Effective Java一书中推荐的一种单例模式写法。
到此为止,我们是不是已经非常清晰明了呢?JDK枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例写法成为一种更加优雅的实现。
8.2.7 还原反序列化破坏单例模式的事故现场
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,当下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,则违背了单例模式的初衷,相当于破坏了单例模式,来看一段代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_49.jpg?sign=1738887747-LZGBtb82aExAr0WDdTN6XIsSUywqggea-0-f86115c53575e27801c6c2e87b20a8a6)
编写客户端测试代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_50.jpg?sign=1738887747-cMiEXukYCJIiCotdkAxKYMploKyd2Liw-0-5a164c48fc10d8ab811d2097c4f5c9a0)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_51.jpg?sign=1738887747-2TRaKtQv7XbPGSrRHnZFW2cGwVYu9XEJ-0-409b31989960c2aa8817f68f103bc894)
运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_52.jpg?sign=1738887747-lstntXjM9XXhsWWzryi9tI8Fcdfp88E1-0-185af35afdfbfd89bad09f6a91cca58a)
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,被实例化了两次,违背了单例模式的设计初衷。那么,如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加readResolve()方法即可。优化后的代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_53.jpg?sign=1738887747-AGAwon3kSvQ9NXASymymIrVjKxGUqZlb-0-0490f691d2ae09facd6a08419b26a60b)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_54.jpg?sign=1738887747-PghNxXoC5t7IGQ7RIsOFvAf9PB6e9J8m-0-288a35d294c6a0092dc9631794d869de)
再看运行结果,如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_55.jpg?sign=1738887747-TfIhZR185PEUIJ42ZsvITj01ToGuqcj2-0-08a2ccda27054f3273aa2e00f41b2110)
大家一定会想:这是什么原因呢?为什么要这样写?看上去很神奇的样子,也让人有些费解。不如一起来看JDK的源码实现以了解清楚。进入ObjectInputStream类的readObject()方法,代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_56.jpg?sign=1738887747-J5LH4SNcQvYxdbVhEhyVwjLB1bkW0WhL-0-573a97fd9bd9e7659a775ac2dd96efb4)
可以看到,在readObject()方法中又调用了重写的readObject0()方法。进入readObject0()方法,源码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_57.jpg?sign=1738887747-265omm0DufmcMp2xJEwXfZC3hr9LJsJ5-0-03675fcb65fe88f6aa54de9689e59dd3)
我们看到TC_OBJECT中调用了ObjectInputStream的readOrdinaryObject()方法,源码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_58.jpg?sign=1738887747-MWFs7Z93J5uBZucrzaxNXBCUQOxus1QB-0-c64e0f1b50de2d4b7f14ae385e3c8668)
我们发现调用了ObjectStreamClass的isInstantiable()方法,而isInstantiable()方法的源码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_59.jpg?sign=1738887747-eZYYaVGb288YmlX70E6c3VhF5EKGDWUz-0-c6549e76dcd2950c1b4ccc04fe205f02)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_60.jpg?sign=1738887747-UMw0g6POkBOAuP98wvPTmWtJNr9k0NhJ-0-74a95da1eac4c8cd201090488397ed5a)
上述代码非常简单,就是判断一下构造方法是否为空。如果构造方法不为空,则返回true。这意味着只要有无参构造方法就会实例化。
这时候其实还没有找到加上readResolve()方法就可以避免单例模式被破坏的真正原因。再回到ObjectInputStream的readOrdinaryObject()方法,继续往下看源码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_61.jpg?sign=1738887747-N3XyFdRkUXHGokZob4oe1B4q0QzD3GFK-0-fba23c53dd16c6d2097a27dcd948e3d4)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_62.jpg?sign=1738887747-G3JwsLrU57ZHE9ba5fhlStaf951wvZef-0-ed5a2458737fb415852c38ad413b347b)
在判断无参构造方法是否存在之后,又调用了hasReadResolveMethod()方法,源码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_63.jpg?sign=1738887747-Uh1PPCYK5ZSsynemysauoFezLLRBx607-0-1244c2c7c72a82e90edd217497f4e8ab)
上述代码的逻辑非常简单,就是判断readResolveMethod是否为空,如果不为空,则返回true。那么readResolveMethod是在哪里被赋值的呢?通过全局查找知道,在私有方法ObjectStreamClass()中对readResolveMethod进行了赋值,源码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_64.jpg?sign=1738887747-z9sKDQTitd2RR2Sh8bXcDPnPmhpAwdqh-0-f8e0c6f2fe1618d962e4a2ad8d2067e0)
上面的逻辑其实就是通过反射找到一个无参的readResolve()方法,并且保存下来。再回到ObjectInputStream的readOrdinaryObject()方法,继续往下看,如果readResolve()方法存在,则调用invokeReadResolve()方法,代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_65.jpg?sign=1738887747-oRpLupGcJ1X70td6REMJnIbQoMxYMmq5-0-f3733144b8727fed5d1a215980aa9cbd)
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_66.jpg?sign=1738887747-IfQqU5Z7nS75bKPLmZHZB4lxLyjeUQ5O-0-abfbb69b7c9a7f2e8e87ab732e363dd5)
可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod方法。
通过JDK源码分析可以看出,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上单例对象被实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,则意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗?其实,枚举式单例写法也是能够避免这个问题发生的,因为它在类加载的时候就已经创建好了所有的对象。
8.2.8 使用容器式单例写法解决大规模生产单例的问题
虽然枚举式单例写法更加优雅,但是也会存在一些问题。因为它在类加载时将所有的对象初始化都放在类内存中,这其实和饿汉式单例写法并无差异,不适合大量创建单例对象的场景。接下来看注册式单例模式的另一种写法,即容器式单例写法,创建ContainerSingleton类。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_67.jpg?sign=1738887747-5JKQr0d9PonBr9JT2N5V2q5KZ6iNXfC6-0-502e41bd94a9ee45423ad6684346e3ed)
容器式单例写法适用于需要大量创建单例对象的场景,便于管理,但它是非线程安全的。到此,注册式单例写法介绍完毕。再来看Spring中的容器式单例写法的源码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_68.jpg?sign=1738887747-0Y9qSwE6iaMFtsh5brndftd8uqYm54qt-0-1822ae55af21dd6d1edda15dc40c270d)
从上面代码来看,存储单例对象的容器其实就是一个Map。
8.2.9 ThreadLocal单例详解
最后赠送大家一个彩蛋,线程单例实现ThreadLocal。ThreadLocal不能保证其创建的对象是全局唯一的,但能保证在单个线程中是唯一的,是线程安全的。下面来看代码。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_69.jpg?sign=1738887747-kkDymtIX7eZpFLtgIZmmJYhOApr19Yj0-0-3b492aa9e110d046b346a5b1f2294a90)
客户端测试代码如下。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_70.jpg?sign=1738887747-NyfhxIvaATsjPSDgaTSobHh69GkSYgHq-0-882ce1514267174c6442d7b8c8132e4a)
运行结果如下图所示。
![img](https://epubservercos.yuewen.com/A1F36C/17725769807799506/epubprivate/OEBPS/Images/txt010_71.jpg?sign=1738887747-zN1uLekSDdVoWjJoiT5ukjjgLH39Wobw-0-de2ab0441e312b78d1b1bc1d480ace42)
由上图可知,在主线程中无论调用多少次,获取的实例都是同一个,都在两个子线程中分别获取了不同的实例。那么,ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal将所有对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。