设为首页 加入收藏

TOP

设计模式 - 单例模式之多线程调试与破坏单例(一)
2019-10-10 11:17:42 】 浏览:514
Tags:设计模式 单例 模式 线程 调试 破坏

前言

在之前的 设计模式 - 单例模式(详解)看看和你理解的是否一样? 一文中,我们提到了通过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模式来直观的找出答案。

在关键代码上打断点

  1. 单例类LazySimpleSingletonif (instance == null) 处:

  1. 测试类,多线程入口调用getInstance()处:

开始调试

  1. 启动 debug ,我们可以在调试窗口找到我们启动的线程:

  1. pool-1-thread-1 线程单步执行到if (instance == null) 断点处,观察instance值为null

  1. pool-1-thread-1执行到instance = new LazySimpleSingleton();处等待初始化:

  1. 切换线程 pool-1-thread-2 同样单步执行到 if (instance == null) 断点处,此时观察instance值也为null(这就是我们常说的两个线程同时执行到断代码处):

  1. 同样将pool-1-thread-2执行到instance = new LazySimpleSingleton();处等待初始化:

  1. 显然,这两个线程都满足if (instance == null) 的条件,都应该到对应的代码块中执行实例化操作,那么这两个线程就会分别初始化:

线程 pool-1-thread-1 实例化后:

切换线程 pool-1-thread-2 观察 instance 值已经被初始化了,但是,线程pool-1-thread-2 还是会被实例化一遍:

线程pool-1-thread-2实例化后:

大家是否一目了然了呢?

  1. 将两个线程执行完,看控制台:

大家可以看到,虽然输出打印的对象是同一个,但是,确实是创建了两遍,只不过 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 =
首页 上一页 1 2 3 4 5 下一页 尾页 1/5/5
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇设计模式-行为型-访问者模式 下一篇通俗易懂设计模式解析——迭代器..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目