文章目录
- 单例模式
- 一、单例模式
- 1.饿汉模式
- 2.懒汉模式(单线程)
- 3.懒汉模式(多线程)
- 改进
- 4.指令重排序
- 1.概念
- 2.question:
- 3.解决方法
- 4总结:
单例模式
一、单例模式
单例,就是单个实例
在有些场景中,希望有的类只能有一个对象,通过代码的语法规范,达到编译器强制检查的效果
单例模式保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例
比如很多用来管理数据的对象,就是单个实例的。
1.饿汉模式
//希望有唯一实例
class Singleton {
//单例模式
private static Singleton instance = new Singleton();
//因为是static成员,在Singleton这个类被加载的时候,这里就会创建实例
public static Singleton getInstance(){
//通过getInstance方法来获取到这个实例
return instance;
}
private Singleton(){}
//将构造方法设置为私有的
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
//通过类名调用成员方法,获取到唯一实例
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);//true
}
- 将构造方法设置为私有的,避免再次生成实例。只能通过getInstance()方法来获取类变量创建好的唯一实例
- 唯一实例是在类加载的时候创建的(创建时间早)->饿汉模式(比较急)
- 在饿汉模式中,如果在多线程中,多个线程同时读取同一个变量,是线程安全的。只读不修改。
2.懒汉模式(单线程)
比较从容,在第一次使用的时候,再去创建实例
class SingletonLazy{
private static SingletonLazy instance = null;
//先设置为空
public static SingletonLazy getInstance(){
if (instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){}
}
-
在首次调用getInstance方法的时候,才会真正创建唯一实例
不调用就不会创建。
-
”懒“意味着高效,省略不必要的操作和开销,只在需要的时候才开始进行。
- 违背了单例的要求。
- 懒汉模式在多线程环境下,涉及到了同一个变量的读取和修改,就存在线程安全问题。
3.懒汉模式(多线程)
解决方法:对if的判断操作和创建实例操作进行加锁,使两个操作始终执行在一起。
class SingletonLazy{
private static SingletonLazy instance = null;
//先设置为空
public static SingletonLazy getInstance(){
synchronized(SingletonLazy.class){
if (instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){}
}
- 虽然进行了加锁,但是每次再调用getInstance()方法的时候,都会进行加锁操作。
- 而懒汉模式的线程安全问题,体现在第一次实例法创建。后续创建好实例后的所有调用操作,都是读操作,没有必要进行加锁。
- 加锁是一个开销很大的操作,加锁就可能涉及到锁竞争,一冲突就会引发堵塞等待,涉及线程的调度。
改进
在加锁操作前,再进行一次判断
如果实例未创建,此时存在线程安全问题,需要加锁。如果对象已经创建,此时线程就是安全的,不需要加锁
private static SingletonLazy instance = null;
//先设置为空
public static SingletonLazy getInstance() {
if (instance == null) {
//第一个if判断的是是否要加锁
synchronized (SingletonLazy.class) {
if (instance == null) {
//第二个if判断的是,是否要new对象
instance = new SingletonLazy();
}
}
}
return instance;
}
首次拿到锁的那个进程,一定创建了这个唯一对象。等后续的进程拿到锁后,再次进行判断,就不会创建对象了。
指令重排序可能会对上述代码会产生影响。
4.指令重排序
1.概念
同样也是因为编译器的优化:
编译器为了执行效率,在保证逻辑不变的前提下,可能会调整原来代码的执行顺序,从而提高效率。
在单线程下安全,多线程下可能会存在误判。
在多线程下,创建实例的操作时,new操作可能会引发指令重排序问题
new操作分为三步:第一步一定是先执行的,可能是1 、2、3 或者1、3、2的顺序;来执行。
1.申请内存空间
2.在内存空间上构造对象(执行构造方法)
3.把内存的地址,赋值给instance引用
就类似于:买房 装修 交钥匙 / 买房、交钥匙、装修 最终效果是一样的。
假设线程1按照1、3、2的顺序来执行。当1和3执行完后,3直接进行赋值操作。此时,instance就不在是null了,而是指向的是一个还没有进行初始化的非法对象。此时1、3执行完,还没开始执行操作2,线程2就开始执行了 。线程2先对instance进行判断。此时intance==null 不成立,线程2直接返回instance。但是instance只是一个还没有进行初始化的非法对象。线程2获取的对象是不完全的。会产生严重的问题。
- 也就是说,由于操作指令的执行顺序被优化了,导致实例创建的不完全就被调用了。表面提前符合了判断标准,但是内部还没有进行完全。
- 就类似于:业主买的是精装房,直接交了钥匙,想要拎包入住时,发现没有进行装修还是毛坯房。实际上精装房是包含装修的。那么就是开发商的执行顺序出现了问题,没安排装修就交了钥匙~
2.question:
提问:既然线程1执行到new的过程中,就已经先加锁了。线程2还能new的1、3操作刚完时,就插进来执行吗?
因为线程2的第一个if没有涉及到加锁操作,是完全可以执行的。锁的阻塞等待,一定是两个线程都加锁的时候才会触发。线程1拿到锁时,修改了instance的引用。此时线程2并没有参与锁的竞争,只是进行了第一个if的判断,非空情况下也不会进入if内部进行加锁操作,而是直接进行了返回。因此没有涉及任何阻塞等待。
3.解决方法
采用volatile,用volatile来修饰instence,保证instence在修改的过程中,就不会进行指令重排序的现象了。
class SingletonLazy {
private static volatile SingletonLazy instance = null;
//先设置为空
public static SingletonLazy getInstance() {
if (instance == null) {
//第一个if判断的是是否要加锁
synchronized (SingletonLazy.class) {
if (instance == null) {
//第二个if判断的是,是否要new对象
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
4总结:
单例模式三步走:
1.懒汉模式下的双重if嵌套
2.用Synchronized对第二个if和后续是实例化操作进行加锁
3.用volatile修饰实例,禁止指令重排序。
面试遇到的话,人生如戏全靠演技,要适当藏拙,一步一步优化。从单线程的懒汉模式,到加锁,再到指令重排序
点击移步博客主页,欢迎光临~