设计模式 - 单例模式之多线程调试与破坏单例
欢迎来到阿八个人博客网站。本 阿八个人博客 网站提供最新的站长新闻,各种互联网资讯。 喜欢本站的朋友可以收藏本站,或者加QQ:我们大家一起来交流技术! URL链接:https://www.abboke.com/jsh/2019/1010/116398.html
前言
在之前的 设计模式 - 单例模式(详解)看看和你理解的是否一样? 一文中,我们提到了通过Idea
开发工具进行多线程调试、单例模式的暴力破坏的问题;由于篇幅原因,现在单独开一篇文章进行演示:线程不安全的单例在多线程情况下为何被创建多个、如何破坏单例
如果还不知道如何使用IDEA工具进行线程模式的调试,请先阅读我之前发的一篇文章: 你不知道的 IDEA Debug调试小技巧
一、线程不安全的单例在多线程情况下为何被创建多个
首先回顾简单线程不安全的懒汉式单例的代码以及测试程序代码:
对于这个单例,我们毫无疑问认为它是线程不安全的,至于为什么,接下来使用IDEA
工具的线程debug
模式来直观的找出答案
在关键代码上打断点
单例类LazySimpleSingleton
的if (instance == null)
处:开始调试
启动debug
,我们可以在调试窗口找到我们启动的线程:将
pool-1-thread-1
执行到instance = new LazySimpleSingleton();
处等待初始化:同样将
pool-1-thread-2
执行到instance = new LazySimpleSingleton();
处等待初始化:切换线程 pool-1-thread-2
观察 instance
值已经被初始化了,但是,线程pool-1-thread-2
还是会被实例化一遍:
大家是否一目了然了呢?
将两个线程执行完,看控制台:这就是通过线程调试模式手动控制线程执行顺序来模拟还原多线程环境下,线程不安全的情况
二、改进线程不安全的单例
我们明白了线程不安全的原因是两个线程同时拿到的instance
资源都为null
,从而都进行实例化
那么有没有什么方法能解决呢?当然有,给 getInstance()
加 上 synchronized
关键字,使这个方法变成线程同步方法:
这就解决了之前所说的线程安全问题,但是这样子在线程数量比较多情况下,如果 CPU
分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降;为了解决线程安全和程序性能问题,于是乎有了我们的双重检查式的单例
这里就不再多说了
三、破坏单例
一般情况下,我们创建使用饿汉式单例或双重检查的懒汉式单例是没有问题的,但是在一定情况下,会发生单例被破坏
反射破坏单例
实际情况下,公司一个程序员写了一个单例,但是另外一个程序员,可能比较牛 X,写代码风格有点不一样,他通过反射来调用别人写的接口,这就会出现此单例并非彼单例的情况
这就破坏了单例
演示
在我们写单例的时候,大家有没有注意到私有的构造方法前面的修饰符仅为 private
,如果我们使用反射来调用其构造方法,然后,再调用 getInstance()
方法,应该就会有两个不同的实例
我们以前面说单例的文章中的 LazyInnerClassSingleton
为例,编写反射调用测试代码:
显然,是创建了两个不同的实例
现在,我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常
来看优化后的代码:
再次调用:
执行测试代码:
大家肯定会问,why?
为了一探究竟,我们来看一下 JDK 源码,我们进入 ObjectInputStream
类的 readObject()
方法:
我们看到代码中调用了 ObjectInputStream
的 readOrdinaryObject()
方法,我们继续进入看源码:
发现调用了 ObjectStreamClass
的 isInstantiable()
方法,而 isInstantiable()
里面的代码如下:
判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()
方法:
代码的逻辑其实就是通过反射找到一个无参的 readResolve()
方法,并且保存下来,现在再回到 ObjectInputStream
的 readOrdinaryObject()
方法继续往下看,如果readResolve()
存在则调用 invokeReadResolve()
方法:
请注意这段代码:
原来枚举类单例在静态代码块中就给INSTANCE
赋了值,是饿汉式单例的实现方式
那么同样的,我们能否通过反射和序列化方式进行破坏呢?
先分析通过序列化方式:
我们还是回到JDK
源码:在 ObjectInputStream
的 readObject0()
方法中有如下代码:
我们发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象
因此,枚举对象不可能被类加载器加载多次
那么是否可以通过反射进行破坏呢?我们先来执行以下反射破坏枚举类的测试代码:
报的是 java.lang.NoSuchMethodException
异常,意思是没找到无参的构造方法
那么我们来看一下 java.lang.Enum
的源码,我们发现它只有一个protected
的构造方法:
发现控制台输出如下错误:
原来,在源码中对枚举类型进行了强制性的判断(16384
代表枚举类型),如果是枚举类型,直接抛异常
到此为止也就说明了为什么《Effective Java》推荐使用枚举来实现单例的原因: JDK
枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现
本文中所涉及的源码可在 github 上找到,相关的测试代码在 test 包下:https://github.com/eamonzzz/java-advanced