多线程系列五:面试中常考的单例模式
1.设计模式
在了解单例模式之前我们先要了解设计模式:
设计模式是一种软性规定,遵守了设计模式,代码的下限就被兜住了,类似于棋谱,是大佬设计出来的,让小白程序员也能写出好的代码
设计模式有很多种,不同语言中也有不同的设计模式,设计模式可认为是对编程语言语法的补充
2.单例模式
单例=单个实例(对象),某个类在一个进程中,只应创建出一个实例,原则上不应该有多个,使用单例模式,就可以对代码进行一个更严格的校验和检查,有的代码中,需要使用一个对象来管理/持有大量的数据,此时有一个对象即可,比如一个对象管理10G数据,如果不小心创建了多个对象,内存空间就会成倍增长,机器就顶不住了
唯一对象如何保证呢?
1️⃣:可通过君子约定,写一个文档约定每个接手维护代码的程序员都不能把这个类创建多个实例,但是并不靠谱
2️⃣:希望让机器对代码中指定的类,创建的实例个数进行校验,如果发现创建多个实例了,就直接对编译报错,如果能做到这一点,就可以放心的写代码
**Java语法本身没有直接约定某个对象创建几个实例,就要用一些奇淫巧技来实现,单例模式具体的实现方式有很多,最常见的就是“饿汉模式”和“懒汉模式”两种
2.1饿汉模式
我们先看一下整体代码,接下来我们一一解释
class Singleton{private static Singleton instance = new Singleton();private Singleton(){}public static Singleton getInstance(){return instance;}
}
这个引用就是希望创建的唯一实例的引用
instance
是Singleton类对象里持有的属性,在Java中每个类的类对象只存在一个,类对象static属性自然也只有一个,因此instance指向的这个对象就是唯一的一个对象
其他代码没法new,从根本上让其他人想new也new不了,只能用getInstance
后续其它代码若想使用这个类的实例,就要通过
getInstance()
这个方法来进行获取,不应该在其它代码中重新new这个对象,而是使用这个方法获取到现成的对象
在main中的使用
对于饿汉模式,getInstance直接返回Instance实例,这个操作本质上是读操作,多个线程读取同一个变量,是线程安全的
2.2懒汉模式单线程版
在懒汉模式中,类加载的时候不创建实例,第一次使用的时候才创建实例,在懒汉模式中,代码有读也有写,是线程不安全的
我们先来看一下单线程版的代码,如果首次调用getInstance,那么此时Instance引用为null,就会进入if条件,从而把实例创建出来,如果后续再使用getInstance,由于Instance已经不再是null了,此时就不会进入if,直接返回之前创建好的引用了,这样设定仍可保证该类的实例是唯一一个,与此同时,创建的实例的时机就不是程序启动时了,而是第一次调用getInstance时,这个操作执行时机就不知道了,就看你的实际需求,大概率要比饿汉模式这种方式晚一些,甚至有可能整个程序压根用不到这个方法,也就把创建的操作省下了
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){}public static SingletonLazy getInstance(){if (instance ==null){instance = new SingletonLazy();}return instance;}
}
会造成创建出两个实例的问题
如果单个线程使用这个代码是安全的,但是在多线程情况下,由于线程是抢占式执行,不确定执行的实际,就会创建出多个实例
加上synchronized可以改善这里的线程问题
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){}public synchronized static SingletonLazy getInstance(){if (instance ==null){instance = new SingletonLazy();}return instance;}
}
但是Instance已经创建过了,此时后续再调用getInstance就都是直接返回Instance实例了,此时操作就是纯粹的读操作了,也就不会有线程安全问题了,这时仍然是每次调用都先加锁再解锁,效率就非常低了
接下来我们看看如何改进
2.3懒汉模式多线程版(这是个重点的面试题,主要是考虑为什么要加东西,每个地方解决什么问题要讲清楚)
class SingletonLazy1{private static volatile SingletonLazy1 instance = null;private SingletonLazy1(){}public static SingletonLazy1 getInstance(){if (instance==null){synchronized (SingletonLazy1.class){if(instance==null){instance=new SingletonLazy1();}}}return instance;}
}
下面这个图片非常重要,解释了重要的逻辑
理解双重if判定/volatile:
加锁/解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候,因此后续使用的时候就不必再进行加锁了
外层的if就是判定下看当前是否把Instance实例创建出来了
同时为了避免“内存可见性”导致读取的Instance出现偏差,于是补充上volatile
当多线程首次调用getInstance,大家可能都发现Instance为null,于是又继续往下执行来竞争锁,其中竞争成功的线程再完成创建实例的操作
当这个实例创建完了之后,其他竞争锁的线程就被里层if挡住了,也就不会继续创建其他实例
我们通过一个更形象的例子来理解这个问题
1️⃣:有三个线程,开始执行getInstance
,通过外层的if(instance==null)
知道了实例还没有创建的消息,于是开始竞争同一把锁
2️⃣:其中线程1率先获取到锁,此时线程1通过里层的if(instance==null)
进一步确认实例是否已经创建,如果没有创建,就把这个实例创建出来
3️⃣:当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的if(instance==null)
来确认实例是否已经创建,发现实例已经创建出来了,就不再创建了
4️⃣:后续的线程,不必加锁,直接就通过外层if(instance==null)
就知道实例已经创建了,从而不再尝试获取锁了,降低了开销