目录
- 1.饿汉式的单例模式
- 2.懒汉式的单例模式
-
- [2.1 线程不安全的原因](#2.1 线程不安全的原因)
- [2.2 解决方法](#2.2 解决方法)
- [2.3 其它问题](#2.3 其它问题)
-
- [2.3.1 问题1](#2.3.1 问题1)
- [2.3.2 问题2](#2.3.2 问题2)
1.饿汉式的单例模式
单例模式下每一次获取的实例化对象是同一个,设计单例模式有两种常见的方法,第一种是饿汉式设计单例模式,即在类加载的过程就实例化对象,需要获取时就返回该对象引用。
设计的单例类是SingleInstance,创建的实例化对象是instance,实例化的对象通过getInstance方法获取,构造方法改为私有的,懒汉式的设计方法如下。
java
public class SingleInstance {
//1.实例化对象
public static SingleInstance instance = new SingleInstance();
//2.私有构造
private SingleInstance(){};
//3.getInstance方法
public static SingleInstance getInstance(){
return instance;
}
}
创建一个Test类判断获取实例instance是否为同一个;在类中定义三个SingleInstance类型的变量instance1,instance2,instance3,创建三个线程t1,t2,t3调用getInstance方法分别初始化三个变量,最终判断是否为同一引用。
java
class T{
//1.定义三个引用
static SingleInstance instance1;
static SingleInstance instance2;
static SingleInstance instance3;
public static void main(String[] args) throws InterruptedException {
//2.创建三个线程t1,t2,t3
Thread t1 = new Thread(() -> {
instance1 = SingleInstance.getInstance();
});
Thread t2 = new Thread(() -> {
instance2 = SingleInstance.getInstance();
});
Thread t3 = new Thread(() -> {
instance3 = SingleInstance.getInstance();
});
//启动线程
t1.start();
t2.start();
t3.start();
//阻塞等待
t1.join();
t2.join();
t3.join();
//3.判断
System.out.println(instance1 == instance2);
System.out.println(instance2 == instance3);
System.out.println(instance3 == instance1);
}
}

在饿汉式设计的单例模式下是线程安全的,不需要进行额外的处理。
2.懒汉式的单例模式
懒汉式的单例模式在最初创建类时不会真正的实例化对象,只是先定义一个变量instance,在真正需要使用时再创建对象,通过getInstance方法调用创建。
getInstance方法的创建:
1)如果是首次调用,需要先创建一个实例化对象,然后返回实例化对象;
2)如果非首次调用,直接返回第一次创建的实例化对象,但是在返回前需要先判断是否首次创建,可以在创建语句前加上一个判断语句,首次创建instance为空(null),非首次创建instance不为空(null),判断完成后再执行返回操作。
java
public class SingleInstance {
//1.实例化对象
public static SingleInstance instance;
//2.私有构造
private SingleInstance(){};
//3.getInstance方法
public static SingleInstance getInstance(){
//判断是否为首次创建
if(instance == null){
//首次创建
instance = new SingleInstance();
}
//返回
return instance;
}
}
使用以上设计的单例类实例化对象,创建t1,t2,t3三个线程,定义三个SingleInstance类型的量
instance1,instance2,instance3,调用三个线程进行初始化操作,判断instacne1,instance2,instacne3是否相等。
java
class T{
//1.定义三个引用
static SingleInstance instance1;
static SingleInstance instance2;
static SingleInstance instance3;
public static void main(String[] args) throws InterruptedException {
//2.创建三个线程t1,t2,t3
Thread t1 = new Thread(() -> {
instance1 = SingleInstance.getInstance();
});
Thread t2 = new Thread(() -> {
instance2 = SingleInstance.getInstance();
});
Thread t3 = new Thread(() -> {
instance3 = SingleInstance.getInstance();
});
//启动线程
t1.start();
t2.start();
t3.start();
//阻塞等待
t1.join();
t2.join();
t3.join();
//3.判断
System.out.println(instance1 == instance2);
System.out.println(instance2 == instance3);
System.out.println(instance3 == instance1);
}
}


在多次执行以上程序时,会出现instance1,instance2,instance3不相等的情况,此时设计的单例模式就是线程不安全的。
2.1 线程不安全的原因
以t1线程和t2线程调用getInstance方法为例,分析不安全的原因,假设instance还未被初始化,t1和t2线程执行getInstance方法执行顺序如下:

开始两个线程并发执行,同时调用getInstance方法,然后t1线程先执行if语句的判断,满足条件。

t2线程执行if语句的判断,满足条件,等待执行实例化语句。

t1线程执行实例化对象,赋值给instance,此时是第一次实例化对象 ,假设instance地址为0x1122ff。

t2线程执行实例化对象操作,此时是第二次执行 ,假设地址是0x112245。

最后执行返回操作,第一次获取的实例对象和第二次获取的实例对象不同。

由以上分析可以得到,在并发情况下,if语句的判断可能是并发执行的,在t1线程和t2线程都满足的情况下就会进行实例化对象,导致多次实例化,不满足单例模式的设计。
2.2 解决方法
为了保证if语句执行的操作是安全,可以给if语句执行的操作加锁,保证if语句的执行同一时间只能在一个线程内执行;加锁的逻辑:定义一个锁对象locker,给if语句执行加锁。
java
//锁对象locker
private static Object locker = new Object();
//getInstance方法
public static SingleInstance getInstance(){
//加锁
synchronized (locker){
//判断是否为首次创建
if(instance == null){
//首次创建
instance = new SingleInstance();
}
}
//返回
return instance;
}
2.3 其它问题
以上的设计可以避免多线程情况下由于if语句的并发执行导致的线程不安全,但是还是存在问题。
2.3.1 问题1
如果此时已经将instance初始化,再一次调用getInstance方法,只需要返回instance,但是执行方法逻辑时,会先进行加锁操作,再进行判断是否为首次创建,而锁是需要资源的,频繁的使用锁会导致性能的下降。
解决已经实例化的问题,可以在加锁前再加上一层if语句的判断,判断是否已经初始化instance。
java
//锁对象locker
private static Object locker = new Object();
//getInstance方法
public static SingleInstance getInstance(){
//第一层if判断是否已经初始化
if(instance == null) {
synchronized (locker) {
//第二层if判断是否为首次创建
if (instance == null) {
//首次创建
instance = new SingleInstance();
}
}
}
//返回
return instance;
}
2.3.2 问题2
在程序执行前,编译器会对程序的性能进行优化,在不改变执行结果的前提下,对部分指令的执行顺序进行重新排序或者执行方式发生改变,以达到时间或者空间的优化,但是这种优化在并发的情况下,就有可能导致线程不安全。
例如:创建一个Test类,定义一个变量count,初始化为0,创建两个线程t1,t2,t1执行死循环操作,执行前需要判断count变量为0;t2线程执行count修改操作,将count修改成不为0。
java
class Test{
static int count = 0;
public static void main(String[] args) {
//判断操作
Thread t1 = new Thread(() -> {
while(count == 0){
//死循环
}
});
//修改操作
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.print("请输入任何0以外的数字最终t1线程:");
count = in.nextInt();
});
t1.start();
t2.start();
}
}
在执行以上程序输入任意数字后,t1线程并没有终止,而是继续执行。


以上线程没有执行完成就是编译器优化导致的,t1线程中count == 0的语句需要涉及的操作是:
1)从内存中将count变量读取的cpu中的寄存器中;
2)在寄存器中执行比较操作;
3)将比较的结果重新写入到内存中;

由于线程t1只执行判断的逻辑,每一次都需要在内存中读取,再到cup执行判断语句,编译器为了更快的执行逻辑,会将内存的读取操作优化到寄存器直接读取(cpu的执行速度比内存的执行更快),即将count变量拷贝一份到寄存器中,当t2线程输入时,对t1线程中的判断操作不影响,导致无法真正终止t1线程。
为了防止编译器优化导致的线程问题,可以对变量加上volatile关键字,修饰后变量的读取就需要在内存中。
java
static volatile int count = 0;
在设计的单例模式中也有可能出现由于编译优化或者指令重排序导致的线程安全问题,所以需要在instance中加入volatile关键字,预防指令重排序和编译器优化问题。
java
private static volatile Object locker = new Object();
多线程下设计的单例模式如下:基于懒汉式设计的单例模式
java
public class SingleInstance {
//1.实例化对象
public static SingleInstance instance;
//2.私有构造
private SingleInstance(){};
//3.getInstance方法
//锁对象locker
private static volatile Object locker = new Object();
public static SingleInstance getInstance(){
//第一次if判断是否已经初始化
if(instance == null) {
synchronized (locker) {
//第二次if判断是否为首次创建
if (instance == null) {
//首次创建
instance = new SingleInstance();
}
}
}
//返回
return instance;
}
}
以上就是关于多线程下设计的单例模式,如果有相关问题,欢迎留言评论,我们下一篇文章,再见!