什么是设计模式:
1.设计模式是一种被广泛接受的解决软件设计问题的经验总结,它们是一种被重用的解决方案的模版,可以在设计和开发过程中解决常见的设计问题。设计模式提供了一种优雅和灵活的设计方式,可以增强软件的可维护性、可扩展性和可重用性。
2.设计模式也是一种程序员的棋谱。(大佬们把一些典型的问题场景整理出来,并针对这些场景,代码怎么写,具体的方案,给出了一些指导和建议)
单例模式就是设计模式的一种
单例模式:
单例模式的含义: 单例模式就是单个实例(对象),强制要求某个类在某个程序中,只有唯一实例(不允许创建多个实例,不允许new很多次)
只有一个实例,在开发过程中,这样的要求很常见。(比如储存数据库信息这样的对象,由于数据库只要有一份就行,即使搞了多个对象,没啥意义,我们只要储存数据在一个数据库里面就行)
单例模式:强制要求一个类不能创建多个对象
单例模式的实现方式:
1.饿汉方式:
尽早的创建实例
尽早的创建实例:在类加载的阶段就创建好实例。
![](https://i-blog.csdnimg.cn/direct/0ac225e05e974755b28076d17f937b48.png)
2.懒汉模式:
"懒"和"饿"是相对的,所以懒汉模式就是尽量晚的创建实例:
![](https://i-blog.csdnimg.cn/direct/7c1b705fe29a465988b668ec0312af44.png)
![](https://i-blog.csdnimg.cn/direct/a93721fc71164d398660f4f9fe11f306.png)
单例模式还有其他写法,这里不过多介绍。
其实上述的饿汉模式/懒汉模式,两者都是和多线程有关系的。
经典面试题:(饿汉/懒汉模式是否是线程安全的,如果不是,该怎么优化)
下面我们看(饿汉/懒汉两种模式),在调用getInstance方法时,会不会出现线程安全问题:
1.饿汉模式:
![](https://i-blog.csdnimg.cn/direct/f58224d87686452a945674249b5380e7.png)
我们仔细观察代码,发现:在调用getInstance方法时,该方法内只存在return操作(也就是读取操作),读取操作是原子的,所以天然线程安全。
2.懒汉模式:
![](https://i-blog.csdnimg.cn/direct/7b7dd702c26141b1acc07b211567fd46.png)
这里我用图来解释:假设有两个线程(t1,t2)同时调用这个方法:
![](https://i-blog.csdnimg.cn/direct/43621d86404a49a29f90304b003054a2.png)
虽然最终结果,第二个实例对象将第一个实例对象覆盖掉了,第一个实例化对象还会被GC释放掉,但是,像这里的实例的对象如果很大的话(比如一次实例对象就要花费十分钟,那这里实例化两次对象就会很消耗资源)
如何解决上面这个问题呢?
首先想到的是:synchronized加锁(常规手段)
我们使用synchronized关键字的地方也要正确,不能加在if语句里面:
![](https://i-blog.csdnimg.cn/direct/4fc477e3786b4406b60522a0db33ed1f.png)
如果加在if语句里面,也会造成线程安全问题:if判断语句和下面synchronized加锁的语句是两个不同的指令,于是会出现上面那种问题(不加synchronized关键字的问题)。
这里,我们需要将判断语句和修改语句都打包成一个原子操作。
![](https://i-blog.csdnimg.cn/direct/fc2a8f7bfa4d480b91f0bf33d8042be7.png)
这样操作之后,t2线程就会阻塞在加锁的地方,直到t1线程进入条件,进行了修改之后,释放了锁之后。才轮到t2线程执行,但是此时instance不等于null,也就不会进入if语句里面。
加锁引入新问题:
当此时如果实例已经创建好了(单例模式),后续再调用getInstance()方法,就只调用if语句和return操作,单纯的读取操作(不涉及线程安全问题)
注意:虽然后续读取不涉及线程安全问题,但是每次调用getInstance()方法,就会触发一次加锁操作,在多线程下,这里每次调用都会加锁阻塞,执行效率就会变低。
解决方法:按需加锁。
按需加锁:真正涉及线程安全问题时,就加锁,不涉及线程安全问题时,就不加锁。
所以,我们只需要在加锁前面,再加一个if语句**(用来判断是否需要加锁)**
![](https://i-blog.csdnimg.cn/direct/8229583e40734b5eae4e8da8de48c34c.png)
虽然上述代码进行了两次if判断,但是每次if判断的意义是不一样的。
问题还未得到根本的解决,如下
内存可见性问题:
之前提到过cpu缓存(L1缓存,L2缓存,L3缓存),每个cpu核心都有自己的缓存,线程可能从缓存中读取数据,而不是直接从主内存读取,因此,一个线程对变量的修改可能只更新了缓存的数据,而没有立即写回主内存,导致其它线程看不到最新的值。
简单来说:一个线程修改了共享变量,另一个线程读取到的依然是旧的值。
volatile关键字:
volatile关键字第一个作用:确保共享变量的修改对所有线程可见。(确保每次读取操作都是读内存)
![](https://i-blog.csdnimg.cn/direct/ff0bce8d1ab843269f14f28943e48b43.png)
指令重排序:
**指令重排序:**java编译器或处理器为了提高程序性能而对指令执行顺序进行重新排序的优化技术。在多线程环境下,这可能导致程序行为和预期不一样。
指令重排序的影响:
单线程中,指令重排序是不会影响结果的,但是在多线程下环境下,可能导致线程间出现不一致的状态。
示例:
场景描述:线程A和线程B。线程A主要负责初始化数据,线程B负责读取数据,如果发生指令重排序,线程B可能会读取到未初始化的数据。
java
public class Test1 {
int x=0;
int y=0;
boolean ready=false;
public void writer(){
x=1;//1
y=2;//2
ready=true;//3
}
public void reader(){
if(ready){
int sum=x+y;//4
System.out.println("sum+"+sum);//5
}
}
}
正常执行顺序:
单线程中:writer方法的执行顺序是:
1.x=1;
2.y=2;
3.ready=true
reader方法的执行顺序是:
4.ready=true
5.int sum=x+y
重排序后的执行顺序:在多线程环境下,由于指令重排序:writer方法的执行顺序可能会变成
1.ready=true
2.x=1
3.y=1
这时如果reader方法在ready=true之后立即执行,但是x和y还未及时初始化,可能会读取到x和y的初始值0,导致输出sum=0
解决指令重排序的方法:
使用volatile关键字第二个作用:加了volatile关键字的变量,关于该变量的读取和修改操作,不会触发指令重排序。
所以上面,在解决内存可见性问题时,同时也可以解决指令重排序的问题。