前言
在之前的 设计模式 - 单例模式(详解)看看和你理解的是否一样? 一文中,我们提到了通过Idea
开发工具进行多线程调试、单例模式的暴力破坏的问题;由于篇幅原因,现在单独开一篇文章进行演示:线程不安全的单例在多线程情况下为何被创建多个、如何破坏单例。
如果还不知道如何使用IDEA工具进行线程模式的调试,请先阅读我之前发的一篇文章: 你不知道的 IDEA Debug调试小技巧
一、线程不安全的单例在多线程情况下为何被创建多个
首先回顾简单线程不安全的懒汉式单例的代码以及测试程序代码:
/**
* @author eamon.zhang
* @date 2019-09-30 上午10:55
*/
public class LazySimpleSingleton {
private LazySimpleSingleton(){}
private static LazySimpleSingleton instance = null;
public static LazySimpleSingleton getInstance(){
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
// 测试程序
@Test
public void test() {
try {
ConcurrentExecutor.execute(() -> {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + instance);
}, 2, 2);
} catch (Exception e) {
e.printStackTrace();
}
}
对于这个单例,我们毫无疑问认为它是线程不安全的,至于为什么,接下来使用IDEA
工具的线程debug
模式来直观的找出答案。
在关键代码上打断点
- 单例类
LazySimpleSingleton
的if (instance == null)
处:
- 测试类,多线程入口调用
getInstance()
处:
开始调试
- 启动
debug
,我们可以在调试窗口找到我们启动的线程:
- 将
pool-1-thread-1
线程单步执行到if (instance == null)
断点处,观察instance
值为null
;
- 将
pool-1-thread-1
执行到instance = new LazySimpleSingleton();
处等待初始化:
- 切换线程
pool-1-thread-2
同样单步执行到if (instance == null)
断点处,此时观察instance
值也为null
(这就是我们常说的两个线程同时执行到断代码处):
- 同样将
pool-1-thread-2
执行到instance = new LazySimpleSingleton();
处等待初始化:
- 显然,这两个线程都满足
if (instance == null)
的条件,都应该到对应的代码块中执行实例化操作,那么这两个线程就会分别初始化:
线程 pool-1-thread-1
实例化后:
切换线程 pool-1-thread-2
观察 instance
值已经被初始化了,但是,线程pool-1-thread-2
还是会被实例化一遍:
线程pool-1-thread-2
实例化后:
大家是否一目了然了呢?
- 将两个线程执行完,看控制台:
大家可以看到,虽然输出打印的对象是同一个,但是,确实是创建了两遍,只不过 pool-1-thread-2
实例化后将 pool-1-thread-1
实例化的对象值给覆盖了。
当我将线程pool-1-thread-1
和线程pool-1-thread-2
同时执行到instance = new LazySimpleSingleton();
处然后先让pool-1-thread-1
执行完打印后,再将pool-1-thread-2
执行实例化操作,就会看到打印的对象会是不一样的了:
这就是通过线程调试模式手动控制线程执行顺序来模拟还原多线程环境下,线程不安全的情况。
二、改进线程不安全的单例
我们明白了线程不安全的原因是两个线程同时拿到的instance
资源都为null
,从而都进行实例化。那么有没有什么方法能解决呢?当然有,给 getInstance()
加 上 synchronized
关键字,使这个方法变成线程同步方法:
public class LazySimpleSingleton {
private LazySimpleSingleton(){}
private static LazySimpleSingleton instance = null;
public synchronized static LazySimpleSingleton getInstance(){
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
当我们将其中一个线程执行并调用 getInstance()
方法时,另一个线程在调用 getInstance()
方法,线程的状态由 RUNNING
变成了MONITOR
,出现阻塞。直到第一个线程执行完,第二个线程才恢复 RUNNING
状态继续调用 getInstance()
方法
这就解决了之前所说的线程安全问题,但是这样子在线程数量比较多情况下,如果 CPU
分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降;为了解决线程安全和程序性能问题,于是乎有了我们的双重检查式的单例。这里就不再多说了。
三、破坏单例
一般情况下,我们创建使用饿汉式单例或双重检查的懒汉式单例是没有问题的,但是在一定情况下,会发生单例被破坏。
反射破坏单例
实际情况下,公司一个程序员写了一个单例,但是另外一个程序员,可能比较牛 X,写代码风格有点不一样,他通过反射来调用别人写的接口,这就会出现此单例并非彼单例的情况。这就破坏了单例。
演示
在我们写单例的时候,大家有没有注意到私有的构造方法前面的修饰符仅为 private
,如果我们使用反射来调用其构造方法,然后,再调用 getInstance()
方法,应该就会有两个不同的实例。
我们以前面说单例的文章中的 LazyInnerClassSingleton
为例,编写反射调用测试代码:
@Test
public void testReflex() {
try {
// 很无聊的情况下,进行破坏
Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
// 通过反射拿到私有的构造方法
Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
// 设置访问属性,强制访问
c.setAccessible(true);
// 暴力初始化两次,这就相当于调用了两次构造方法
LazyInnerClassSingleton o1 =