前言:
?上期博客:【JavaEE初阶】深入理解wait和notify以及线程饿死的解决-CSDN博客
?感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客
⭐️小编会在后端开发的学习中不断更新~~~
?非常感谢你的支持
目录
?️1.引言
?️2.两种单例模式
2.1单例模式设计意义
2.2饿汉模式
1.饿汉模式介绍
2.饿汉模式实现
3.饿汉模式测试
2.3懒汉模式
1.懒汉模式介绍
2.懒汉模式实现
3.懒汉模式测试
2.4两种单例模式的区别
?️3.线程安全问题
3.1两种模式线程安全
3.2懒汉模式线程安全分析和解决
1.随机调度问题
解决办法
2.重复上锁问题
解决办法
3.指令重排序问题
解决办法
?️4.总结
?️1.引言
OK啊!!!小伙伴们,在上期继小编讲解过wait和notify的使用的问题后,本期将开始实现关于开发中常用的单例模式,那么废话不多说,直接步入正题,go go go~~~;
且听小编讲解,包你学会!!!
?️2.两种单例模式
2.1单例模式设计意义
单例模式(单个实例对象)即一种设计模式,是为我们程序猿需要掌握的一门技能,遵守设计模式,可以保住我们写代码的技术下限;
最主要的原因:在某个进程中只能创建一个实例,所以使用到单例模式可以帮助实现对我们的所写代码的检查和校验;
为啥要实现单例模式:例如我们需要使用一个对象对大量数据进行管理,假如这些数据有10GB,所以一旦不小心多创建一个实例对象,那么消耗的内存空间就会成倍进行增长;所以我们需要机器检查人为创造多个实例时就会报错,实现只存在一个实例对象~~~
2.2饿汉模式
1.饿汉模式介绍
所谓的饿汉,即十分迫切的意思,这里在单例模式设计中,表示实例一旦被加载了,这个实例就已经创建了,即实例的创建非常早,相当于程序一启动,那么实例就已经创建了~~~
2.饿汉模式实现
代码实现如下:
class Singleton{ private static Singleton instance=new Singleton();//即类对象这里只有唯一的静态类 //实例化后,如果其他线程要进行调用 public static Singleton getInstance(){ return instance; } //将构造方法定位私有,就无法进行new操作 private Singleton(){}}
代码解释:
private static Singleton instance=new Singleton();//即类对象这里只有唯一的静态类
这里就是创建了一个名字为instance的静态对象变量,并初始化为一个Singleton的类的实例,每个类的类对象只有一个,类对象中的静态属性也只有一个,所以instance所指的对象也只有一个~~~
public static Singleton getInstance(){ return instance; }
这里即创建了一个静态方法,由于实例对象的访问范围是一个私有的,所以要借助这个方法实现在其他类使用这个私有对象;
private Singleton(){}
这里的私有构造方法是为了实现单例模式所使用的一种写法,可以导致其他类中在使用这个对象时,就无法进行new操作,无法创建一个新的实例;
3.饿汉模式测试
1.当我们实现一个new一个新的实例的时候,我们可以发现此时代码直接报错了;
如图所示:
解释:很明显可以看到此时报错为:这个Singleton为私有属性,不能创建一个新的实例,这就实现了单例模式;
2.当我们通过“==”符号进行比较地址的时候,可以发现此时为“true”;
代码实例:
public static void main(String[] args) { Singleton s1=Singleton.getInstance(); Singleton s2=Singleton.instance; System.out.println(s1==s2); }
那么很明显就是小编这里的instance静态对象访问权限为共有的,所以这里可以通过类名直接调用,一般情况下饿汉模式这里的实例对象为私有的哈~~~
2.3懒汉模式
1.懒汉模式介绍
这里和上述的饿汉模式,有异曲同工之妙,这里的懒是指创建的实例的时间在啥时候使用,那么就啥时候创建实例~~~
2.懒汉模式实现
代码实现如下:
class Lazyton{ private static Lazyton instance=null; public static Lazyton getInstance(){ if (instance==null){ instance=new Lazyton(); } return instance; } private Lazyton(){}}
代码解释:
private static Lazyton instance=null;
这里即创建了一个静态对象,但是这里并没有进行实例化,是为了下方调用方法时来进行必要的实例化对象;
public static Lazyton getInstance(){ if (instance==null){ instance=new Lazyton(); } return instance; }
这里就是懒汉模式的精髓了,通过调用方法来实现对象的实例化,在进行实例化之前我们要判断这个对象是否已经被实例化过了,如果没有就进行实例化,如果有那么就直接返回这个对象;
private Lazyton(){}
最后这里实现构造方法的私有化,实现在其他类中不能可直接通过new关键词来实例化新的对象;
3.懒汉模式测试
这里的测试和上述的饿汉模式基本一致,只不过是创建实例的时间不同而已~~~
如图所示:
并且获取的两个对象是一样的,那么测试输出就为“true”,小编这里就不再过多演示了~~~
2.4两种单例模式的区别
这两种单例模式的区别最主要就是两者创建实例的时间不同,这会带来什么好处呢?
注意:
1.第一种情况
在某些程序中,是不需要创建的实例的,而饿汉模式会导致,不管这个程序使用或者不使用,都会创建这个实例对象;然而在不需要实例的情况下使用懒汉模式会比使用饿汉模式省下创建实例这个操作,提升效率~~~;
2.第二种情况
在某些情况下,假如有一个文件有10GB,使用饿汉模式,编辑器就会将10GB的数据加载到内存当中,然后统计展示;而使用懒汉模式,编辑器会展示一小部分数据,随着用户翻页,再次加载一部分,而不是全部加载出来;这会导致饿汉模式的情况下效率低,而且不符合实际~~~
?️3.线程安全问题
3.1两种模式线程安全
在上述我们举例说明了关于两种模式的区别和实现,我们可以发现饿汉模式感觉是不如懒汉模式的那么真的是这样吗???答案是否定的;
注意:由于饿汉模式在创建实例的时候,是直接返回实例对象的如下代码:
public static Singleton getInstance(){ return instance; }
那么此时就可以发现饿汉模式只有“读”操作,我们知道只有读操作是线程安全的
注意:此时我们可以发现对比之下,懒汉模式中:
if (instance==null){ instance=new Lazyton(); }
那么此时可以发现懒汉模式既有“读”操作,又有“写”操作,那么此时我们就知道此时线程是不安全的,存在问题,那么问题是什么呢???
3.2懒汉模式线程安全分析和解决
1.随机调度问题
由于线程的随机调度的特性,如果存在两个线程随机调度,就有以下的情况:
解释:此时我们就可以看到,若第一个线程在执行到if语句的时候,满足条件,突然被调度走了,那么此时线程2,插入直接实例化了一个对象,然后线程1又被调度回来,执行剩下的语句,又创建了一个实例,那么此时就创建了两个实例,这就不是单例模式了~~~
解决办法
此时我们就可以使用前面讲解的synchronized来实现这段代码打包成原子性的代码,这里即可解决
代码实现如下:
public static Lazyton getInstance(){//由于线程安全问题,加锁 synchronized (lock){ if (instance==null){ instance=new Lazyton(); } } return instance; }
此时若加锁后,即使被调度走了,由于锁的竞争,会导致线程2进入阻塞状态,保证线程1能够执行完;所以就解决了这个随机调度的问题~~~
2.重复上锁问题
为啥会重复进行上锁呢???我们一起看看代码:
synchronized (lock){ if (instance==null){ instance=new Lazyton(); } }
解释:首先我们可以发现不管谁调用这个方法,都会使用synchronized,来进行上锁,我们希望的是在进行实例化过后,这个锁就不要再添加了,即调用这个方法直接返回这个对象实例,如过一直进行加锁,会导致代码执行的效率不高,与“高性能”就无关了~~~
解决办法
这里我们就可以添加一个判定是否需要添加锁的条件,即一个if语句,这里就可以解决问题了
代码如下:
public static Lazyton getInstance(){//由于线程安全问题,加锁 if (instance==null){ synchronized (lock){ if (instance==null){ instance=new Lazyton(); } } } return instance; }
那么此时我们在进行加锁的时候,判定这个实例是否已经被创建了,若创建了就不用进入加锁,进行下面的代码片段了,直接返回实例,反之就加锁,进行实例化对象,这就提高了执行效率~~~
3.指令重排序问题
这里是由指令重排序引起的线程安全问题,和内存可见性问题一样,也是编译器优化程序的一种方式,为啥重新排列会提高效率呢???
举个例子:
此时我们买东西的效率就大大提升了,同理这里的指令执行也是一样的~~~
instance=new Lazyton();
这里的代码可以拆成三个步骤:
1.申请一段内存空间
2.在这个内存上调用构造方法,创建实例
3.将这个内存地址赋值给instance引用变量
那么此时一般来说执行步骤为123,但是编译器可能会优化成132~~~,那么就造成了一下的问题
如图所示:
此时由于随机调度的问题,此时就会导致如上图所示的情况;
在申请内存空间后,并将地址赋值给instance引用变量时,突然被另一线程抢占执行后,发现此时不满足instance==null那么就会直接跳出if条件语句;
注意:这里如果此时线程二还有其他的代码运用这个实例的操作的属性和方法,可能就会导致一个严重的问题,因为这些属性都是未初始化的为“0”的值,那么这里就可能导致代码的逻辑问题;
解决办法
这里就要使用I一个关键词,即volatile,volatile的作用:
1.保证内存可见性问题,每次访问都要读取内存中的数据,而不会优化到寄存器中;
2.禁止指令重排序,对于volatile修饰的变量的读或者写操作的相关指令,是不能重写排序的
那么解决以上问题后,代码如下:
private static Object lock=new Object(); private static volatile Lazyton instance=null; public static Lazyton getInstance(){//由于线程安全问题,加锁 if (instance==null){ synchronized (lock){ if (instance==null){ instance=new Lazyton(); } } } return instance; }
那么此时就是通过volatile修饰instance引用变量后,保证进行实例化时不会导致指令重排序而造成的线程安全问题了~~~
?️4.总结
??小编本期讲解了关于设计模式之单例模式中的两个重要模式,即饿汉模式和懒汉模式,关于他们的实现代码,小编也进行了编写;以及最重要的两个模式的线程安全问题,小编进行了注重分析问题的产生,以及如何解决都有涉及~~~
???~~~~最后希望与诸君共勉,共同进步!!!
???以上就是本期内容了, 感兴趣的话,就关注小编吧。
?? 期待你的关注~~~