线程安全的单例模式是面试中常见的问题,所以熟练掌握这种单例模式尤为重要
什么叫单例模式?
单例模式就是一种设计模式,写代码时一种常见的应用场景,设计模式就是针对于这些应用场景给出的解决方案,我们可以把它想成为棋谱,这种棋谱就是由那些“围棋高手”(大佬程序猿)留下来便于小白使用
而且我们还需要知道单例模式的效果就是保证某个类只有唯一实例
单例模式分为饿汉模式和懒汉模式
顾名思义,饿汉模式就是表示比较着急的去干某件事,这里的饿汉并不是代表真的饿了,而是代表急不可待的意思
举个 :小明假期留了三本大作业,结果放假第一天,小明就照着答案把这些作业全都写完了,于是小明的假期就变的甚是潇洒,显然,这种做法是不可取的,虽然速度提上去了,但是并没有什么卵用
我么可以引出饿汉的单例模式就是比较着急的去创建线程
下面我们就用代码来演示一下饿汉的单例模式
创建一个Singleton类(我们要知道singleton这个单词的意思,如果singleton大家不知道那么singledog的意思大家一定要知道,没错就是你“单身狗 的意思”,那么singleton就是单个的意思),通过这个类来保证单例模式只有唯一的实例
*
线程安全的单例模式实现-饿汉模式
*/
class Singleton{
//1.使用static创建一个实例,并且立即进行实例化,
//这个instance对应的实例,是该类的唯一实例.
private static Singleton instance = new Singleton();
//2.为了防止程序猿在其他的地方不小心的new了这个Singleton,把构造方法设为private
private Singleton(){}
//3.提供一个方法,让类外能拿到这个唯一实例
public static Singleton getInstance(){
return instance;
}
}
public class TestDemo1 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
}
}
上述代码中,static是用来修饰类成员(类属性/类方法),一个Java程序中,一个类对象只存在一份(JVM保证的),这样也就保证了static修饰的实例只有一份了
static是单例模式实现的主要方法
举 :小红和小明是同班同学,小明为了能够愉快的过假期,一天的时间内写完了所有的作业,小红不同,小红是个好学生,她给自己定了个计划,每天写一点,而且很认真独立完成作业,在照着答案批改,写完作业后再去玩,就这样,小红用了一个假期写完了所有作业,开学后,小明受了老师的惩罚,小红却得到了表扬
写到这里,可能就会有同学会问,那为什么把这种模式叫做懒汉模式呢
我们需要知道,在计算机中懒是一种褒义词,并不是我们日常生活中的懒,
这种懒会让系统变得更高效,系统什么时候有时间,系统再去工作,而不是一股脑的把工作全给做完
我们可以看出懒汉模式的单例模式就是什么时候需要去做的时候,系统才回去创建实例,而不是一股脑的把实例全都创建出来
下面我们就用代码来演示一下懒汉的单例模式`
*
线程安全的单例模式实现-懒汉模式(只有真正的使用到getInstance的时候才真正的创建实例)
*/
class Singleton2{
//1.和饿汉模式有所不同,此模式并不是立即初始实例
private static volatile Singleton2 instance = null;
//2.将构造方法设为private
private Singleton2(){}
//3.提供一个方法获取实例,但是只有真正需要的时候才会真正的去创造实例
public static Singleton2 getInstance(){
if(instance==null){
instance = new Singleton2();
}
return instance;
}
}
和饿汉模式有所不同,此模式并不是立即初始实例,而是判断一下是否真的需要,只有在真正的需要的时候才会去创建实例
以上是懒汉模式的雏形,但是,我们随着深入了解,会发现,懒汉模式存在许多问题
if(instance==null){
instance = new Singleton2();
}
这两行代码即包含了读操作,也包含了修改操作,而且这两行代码是两个步骤,不是原子性的(何为原子性,可以去了解我之前写的线程安全问题的博客),也就是说此代码存在线程安全问题
线程安不安全,具体的多线程环境下,并发的调用getInstance,是否存在BUG
如图解:我们不难看出,如果读操作和修改操作不是原子性的,就可能会导致多个线程创造出多个实例出来,就违背了我们的初衷,实现多线程的单例模式
所以,我们需要加锁操作:
synchronized(Singleton2.class){
if(instance==null){
instance = new Singleton2();
}
return instance;
}
}
上述代码使用了类对象作为了锁对象,类对象在程序中只存在一份,就能保证了多线程在调用getInstance的时候都是针对同一个对象进行的加锁
这样就保证了多操作和修改操作的原子性
加了锁之后线程变得安全了,但是又产生了新的问题
加锁虽然解决了线程安全问题,即开发效率,但是运行效率也随着降低,我们需要保证开发效率,也需要保证运行效率
改进方案:让instance初始化之前,才进行加锁,初始化之后就不需要加锁了,所以在加锁之前,加上一个判定条件,条件就是当前instance是否已经完成
if(instance==null){
synchronized(Singleton2.class){
if(instance==null){
instance = new Singleton2();
}
}
}
return instance;
}
有细心的同学可能会发现,上述两个if的判断条件是相通的,那么将所有的代码放在一个if条件里不就好了吗?
这是绝对不可以的!!!
因为代码如果是这样的
if(instance==null){
synchronized(Singleton2.class){
instance = new Singleton2();
}
}
return instance;
很明显,这样的代码没有保证读操作和修改操作的原子性,这也就代表着之前的所有铺垫都形同虚设了
那为什么会出现这种情况呢?这完全是一种美丽的巧合
1.第一个判定条件,是否需要加锁
2.第二个判定条件是否需要对instance初始化
这两个判定条件起到的效果/预期的目的是完全不一样的
碰巧这两个判定条件都是instance是否为null
在解决了上述代码后,还有最后一个重要的线程安全问题
我么发现,上述代码如果是多个线程共同调用这里的getinstance,因为这个方法只有一个(单例模式),就会导致方法会有大量的读操作产生,前面我们说到,如果代码有大量的读操作,编译器就会把这个读内存操作优化成读寄存器操作,这也就是我们所说的线程安全问题之一:内存可见性,这可能会引起第一个if判定条件产生误差,第二个不会产生影响,因为关键字synchronized,所以我们需要对instance这个变量进行修饰
volatile可以保证变量不被编译器优化,但是不能保证原子性,此处我们只需要防止编译器优化即可
//1.和饿汉模式有所不同,此模式并不是立即初始实例
private static volatile Singleton2 instance = null;
//2.将构造方法设为private
private Singleton2(){}
//3.提供一个方法获取实例,但是只有真正需要的时候才会真正的去创造实例
public static Singleton2 getInstance(){
if(instance==null){
synchronized(Singleton2.class){
if(instance==null){
instance = new Singleton2();
}
}
}
return instance;
}
单例模式是我们秋招面试时非常重要的一个问题,懒汉模式尤为重要,懒汉模式产生的问题我们也需要熟练掌握
1. 在正确的位置加锁,保证读操作和修改操作的原子性
2. 双重if判定,避免产生不必要的锁竞争
3. 对instance使用关键字volatile,防止编译器优化
以上就是对线程安全中单例模式的总结,此案列非常重要,是多线程经典的案例,大家应该熟练掌握相关代码,最后可以写出来