单例设计模式详解一:不安全的懒汉式(多线程环境验证)

时间:2022-11-04 09:58:02

单例设计模式详解一:不安全的懒汉式(多线程环境验证)

写在前面的话

前言:虽然工作也有 2 年了,自己也拼了命一样的自己学东西,但很感慨,很多问题都没有深入研究。最近正好有时间,仔仔细细研究了一下单例设计模式。在自己的博客中做一下记录。

单例的英文写法是:Singleton,面试之前一定要记下来这个单词的写法。

为什么要有单例的类呢?
有些对象的创建消耗时间和内存是非常大的,恰恰好这些对象在我们的应用中只需要使用 1 个,如果不能得到控制,会造成资源的浪费。
说明:就想我们的办公室、家里那些很贵的电器,比如电冰箱、空调、打印机、热水器这一类电器,一般情况下,在一个小范围内我们只用使用 1 个。比如我们办公室吧, 1 台打印机就够我们几个人用了,没有必要买 2 台打印机。类似地,Java 中有这样的一些对象,例如线程池、数据库连接池,一个应用程序中,我们只需要有 1 个这样的大对象。

很多朋友们学习设计模式,最先是从单例设计模式开始的,很容易地,我们知道,单例设计模式有两种写法:懒汉式与饿汉式,分别如下。

饿汉式

public class Singleton {
// 把"唯一的"对象保存在单例类的属性里
private static Singleton instance = new Singleton();

// 构造器私有化,不能在类的外部随意创建对象
private Singleton(){}

// 提供一个全局的访问点来获得这个"唯一"的对象
public static Singleton getInstance(){
return instance;
}
}

说明:类加载的时候就创建对象。
优点:简单清楚,代码“看起来”优雅,很安全。(这里的“安全”,后面会解释,为什么会很安全)。
缺点:没有实现懒加载。
看了上面的分析,有的朋友要问了,不懒加载的话,又有什么关系呢?就让应用程序启动的时候一起加载就好了呀。“安全”和“懒加载”,两害相较取其轻,我要“安全”,损失一些所谓“性能”,看起来饿汉式的单例模式写法可以说相对“完美”一些。

懒汉式

/**
* Created by liwei on 16/11/23.
* 单例设计模式懒汉式写法。
*/

public class Singleton {
// 把"唯一的"对象保存在单例类的属性里
private static Singleton instance = null;

// 构造器私有化,不能在类的外部随意创建对象
private Singleton(){}

// 提供一个全局的访问点来获得这个"唯一"的对象
// 请注意,这样的代码再多线程环境下是有问题的
// 很可能 instance = new Singleton(); 会被执行多次
// 我们可以模拟多线程环境来检验我们的猜想
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

测试类:

public class SingletonPatternDemo {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}

说明:这是单线程情况下,单例测试类(方法)的写法。请记住,我们的程序一般是在多线程情况下运行的,所以请不要用这样的测试类去检验我们的单例设计模式。应该使用下面的测试类(方法)。

验证在多线程环境下懒汉式单例写法的不“安全”之处

为了验证我们的结论,我们修改一下懒汉式单例设计模式的代码:
懒汉式单例设计模式(添加了部分注释和跟踪流程的代码):

public class Singleton {
// 为了记录这个 Singleton 类被实例化的次数
// 声明一个 static 类型的计数器
private static int count;
// 把"唯一的"对象保存在单例类的属性里
private static Singleton instance = null;

// 构造器私有化,不能在类的外部随意创建对象
private Singleton() {
System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
}

// 提供一个全局的访问点来获得这个"唯一"的对象
// 请注意,这样的代码再多线程环境下是有问题的
// 很可能 instance = new Singleton(); 会被执行多次
// 我们可以模拟多线程环境来检验我们的猜想
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

模拟多线程环境下单例设计模式类获得实例的测试代码:

public class SingletonPatternDemo {
public static void main(String[] args) {
Runnable task = ()->{
String threadName = Thread.currentThread().getName();
Singleton s1 = Singleton.getInstance();
System.out.println("线程 " + threadName + "\t => " + s1.hashCode());
};
// 模拟多线程环境下使用 Singleton 类获得对象
for(int i=0;i<1000;i++){
new Thread(task,"" + i).start();
}
}
}

运行结果如下(部分)

Singleton 私有的构造方法被实例化 1 次。
Singleton 私有的构造方法被实例化 9 次。
线程 7 => 674311410
线程 11 => 828929086
线程 10 => 674311410
Singleton 私有的构造方法被实例化 2 次。
线程 2 => 1596980710
Singleton 私有的构造方法被实例化 6 次。
Singleton 私有的构造方法被实例化 4 次。
Singleton 私有的构造方法被实例化 7 次。
Singleton 私有的构造方法被实例化 8 次。
Singleton 私有的构造方法被实例化 1 次。
Singleton 私有的构造方法被实例化 5 次。
线程 17 => 2117422881
Singleton 私有的构造方法被实例化 3 次。
线程 1 => 522589741
线程 3 => 2117422881
线程 16 => 1757887318
线程 0 => 1757887318
......

分析:在懒汉式单例设计模式中,getInstance() 方法由原来饿汉式的 1 行代码

return instance;

变成

if (instance == null) { //(1)
instance = new Singleton();
}
return instance;

很有可能的一种情况是:线程 1 运行到 (1)处的时候,线程 2 抢到的 CPU 的执行权,进入 getInstance() 方法,运行了 instance = new Singleton();,但线程 2 创建了对象这件事情,线程 1 根本不知道,等到线程 1 重新获得 CPU 执行权的时候,从 (1) 处继续执行,又运行了 instance = new Singleton(); 这行代码,这样,“多余的”对象就被创建出来了。

类似地,大家可以验证一下,饿汉式就没有“安全问题”,因为饿汉式单例设计模式在所有线程启动之前就创建好单例类的对象了。你完全可以像我这样“很不专业地”写一个饿汉式设计模式。

// 这个代码是有问题的,不要这样写
// 这个代码是有问题的,不要这样写
// 这个代码是有问题的,不要这样写
public class Singleton {
private static int count;
private static Singleton instance = new Singleton();

private Singleton() {
System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
}

public static Singleton getInstance() {
// 这里的 if 判断纯属多余
if (instance == null) {
System.out.println("------ 检验程序会不会走到这个 if 方法里面来 ------");
instance = new Singleton();
}
return instance;
}
}

同样地,用多线程测试代码去验证,饿汉式单例设计模式的“安全”性。
那么,我们就会想到:
(1)饿汉式单例设计模式就一定是最佳实践吗?虽然它丢失了懒加载的特性。
(2)懒汉式单例设计模式无非就是不安全,那么我们让它安全不就可以了吗?Java 中有同步机制,可以保证我们的代码是“安全”的呀。
带着这些问题,我们在下一篇文章中进行分析。