文章目录
1.什么是单例模式
单例模式是设计模式中的一种,其实设计模式就好好比是一个棋谱,我们在日常下棋的时候会有一些经典的套路。那么在设计模式中也有这样的经典套路。这些经典的套路都是有大佬前辈们实现的。我们在写代码的时候,有很多经典的场景,在经典场景中有一些经典的应对套路。大佬们把这些常见的应对手段给整理起来,就起来个名字–设计模式。有了设计模式,无论是新手程序员还是资深的老程序员,都会有一个代码编程规范。以便让初出茅庐的新手,代码不至于写的很糟糕。
2. 单例模式的组成
单例模式分为:饿汉模式和懒汉模式。单例模式之所以被称为单例模式,是因为我们在创建单例模式类的时候,就把该类的构造方法使用private进行修饰,以便在该类外,不能直接创建出一个实例。
3.饿汉模式实例
饿汉模式:指的是在单例模式中,在对单例进行初始化的时候,直接赋予单例实例,直接new出一个对象。
饿汉模式也可以这样理解:我们平时在自己家里的时候,都洗过碗吧。就比如说,中午这顿饭使用了4个碗,在吃完饭后,我们立即就把4个碗给刷了。这里之所以被称为饿汉模式是因为,在饿汉模式中,创建实例的时候比较着急。在初始化的时候,直接创建实例。
饿汉模式代码案例:
//创建出一个单例模式
//单例模式分为饿汉模式和懒汉模式
class Singleton{
//在饿汉模式中,在进行初始化的时候,直接创建出实例
public static Singleton instance = new Singleton();
//使用private 修饰该类的构造方法,在类外无法创建出一个该类的对象
private Singleton(){}
//在类外调用该类中的实例
public static Singleton getInstance(){
return instance;
}
}
public class TestDemo2 {
public static void main(String[] args) {
//Singleton singleton = new Singleton();
Singleton single = Singleton.getInstance();
}
}
我们尝试在main类中,自己创建出一个SingleTon的实例。
3.1在饿汉模式中为什么在创建实例的时候使用static修饰?
因为 static 修饰的成员更准确的说是类成员,类属性、类方法,不加 static 修饰的成员准确的来说,就是实例方法,实例成员,实例属性。
在一个java程序中,一个类方法只会存在一份(JVM保证的) 这也就是为什么要使用static对实例进行修饰的原因。进一步的就保证了在类的static 成员也只会存在一份。
在这里我们在深究一个static 关键字
其实在我们使用的编程语言java中,static表示的意思和这个单词的字面意思完全不同,static 的意思大家知道 是静态的。这其实是一个历史遗留问题。
在C语言中的static有3个作用:
修饰局部变量,把局部变量的生命周期变长。,修饰一个全局变量,把这个全局变量的作用域限制到整个.c文件。
修饰一个函数,把这个函数的作用域限制到整个.c文件。
我们在这里也可以看出在c语言中static 关键字的英语本意和在c语言中的使用效果,也是对不上号的。
其实在上古时期,那时候的static是表示把变量放到静态内存区中,于是引入了static关键字,但是随着计算机的发展,这个东西就逐渐的没落了。但是static 关键字有被赋予了新的功能。
在C++中 static关键字除了上述C语言的static 功能之外还有新的用法,修饰一个类的成员变量和成员函数,此处static 修饰的成员就表示为类成员。
Java语言就是把C++中static 的功能继承过来了而已。
既然static 关键字的本意和它的对应效果对不上号,那么为什么不使用其他的词呢?
在一个编程语言中,要想新增一个关键字,是一件非常有风险的事情。因为不能还在程序中的单词重合。
SingleTon.class 类对象,就是.class文件被JVM加载到内存中,表现出来的模样。类对象就有着.class文件的所有信息。就像类名,属性等都可以有SingleTon.class中找到。这样也就实现了反射
3.2 判断该实例是否是线程安全的
饿汉模式是线程安全的
那么为什么饿汉模式样式的单例模式是线程安全的呢?我们在程序的哪里判断该单例模式是线程安全的?
线程安不安全,具体是在多线程环境之下,并发调用的getInstance()方法是否会产生bug?
在博主的上一篇文章中,介绍了产生线程不安全的案例。
造成线程不安全的案例有5种。
- 线程抢占式执行,线程间的调度充满了随机性。
- 多线程对一个变量进行修改。
- 针对变量操作不是原子的
- 内存可见性问题
- 指令重排序问题。
我们现在回顾在饿汉模式中的getInstance()方法,在该方法中只有一个return操作,就是对一个变量进行了读取,符合针对变量操作的原子性。所以是线程安全的
4.懒汉模式实例
懒汉模式创建懒汉模式的单例模式的时候,我们不着急创建出实例。
还是那洗碗举例,我们中午吃饭的时候,使用了4个碗,吃完饭后,我们不着急洗碗,到了晚上吃饭的时候,需要使用的几个碗,那么我们现在就洗几个碗。假如说我们晚上要使用2个碗,那么就洗两个,剩下的两个碗不管。
在我们平时生活中,饿汉模式比懒汉模式好,因为你试一试中午吃完饭不洗碗,如果不洗碗肯定会被挨骂。但是在我们的计算机中可未必,懒汉模式要比饿汉模式要好一些。
那么为什么在计算机中懒汉模式要比饿汉模式要好一些呢?
就比如说,现在有一个1G的图片文件,如果按照饿汉模式,那么计算机就会在内存中一下把这1G大的图片文件全部加载出来,这不就耗费CUP资源,并且如果计算机用户在浏览图片文件的时候,就看了一点没有全部把图片文件浏览完。那么这样的饿汉模式不就是费力不讨好好吗?
反而看看我们的懒汉模式,之所以懒,是因为你赶它一些它走一下。在懒汉模式中,我们一次不会再内存中把所有的图片加载完,而是把计算机用户的一个计算机屏幕中的图片加载出来。用户滑动一下滚动条,加载一下。这样就进行了优化。
其实在计算机中懒汉模式是褒义词,但是在现实世界中就算了吧😂
class Singleton2{
//懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
public static Singleton2 instance = null;
private Singleton2(){}
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
public class TestDemo3 {
public static void main(String[] args) {
Singleton2 singleton2 = Singleton2.getInstance();
}
}
4.1 判断该实例是否是线程安全的,如果不是线程安全的,那么怎样修改可以成为线程安全的实例
首先上述的所谓的懒汉模式的单例模式不是线程安全的
那么为什么它不是线程安全的呢?
因为在多线程中,我们调用懒汉模式中的getInstance()方法的时候,针对变量的操作不是原子的,那么有从哪可以看出不是原子的呢?
如图:
那么针对变量操作不是原子性的,它的解决办法就是进行加锁,使用synchronized关键字进行加锁!!!
修改之后的代码:
//实现一个线程安全的单例模
class Singleton2{
//懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
public static Singleton2 instance = null;
private Singleton2(){}
public static Singleton2 getInstance(){
synchronized(Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
return instance;
}
}
public class TestDemo3 {
public static void main(String[] args) {
Singleton2 singleton2 = Singleton2.getInstance();
}
}
我们知道如果遇到可针对变量操作不是原子的,要使用synchronized关键字进行加锁,但是也不是说,代码中有了synchroniezd关键字就一定不会线程安全,我们要把synchronized关键字加对地方。synchronized加的位置正确,不能随便写。
//类对象在一个类中只有唯一一份,就能保证调用的getInstance的时候都是针对都一个对象进行加锁
synchronized(SingleTon2.class){
}
但是我们加锁之后,又带来的新的问题!!!
对于刚才的这个懒汉模式的代码而言,线程不安全发生在instance没有被初始化之前,未被初始化的时候,多线程调用getInstance()方法时,会存在线程安全问题,因为涉及到读和修改。但是在instance初始化之后,instance一定不是null,if条件一定不成立,getInstance()就只剩下两个读操作,也就是说instance初始化之后,线程就是安全的了。
并且按照上述的加锁操作,无论是代码中的instance初始化之前,还是初始化之后。每次调用getInstance()方法的时候,都会对其进行加锁。也就意味着即使初始化之后(已经线程安全了),但是仍然存在大量的锁竞争。
既然这里的instance已经被初始化过了,即使这里的条件在不能被满足了,但是仍然会调用getInstance()方法,都需要进行加锁,也就可能会产生锁竞争,但是我们知道这里的锁竞争其实是没有必要的。
我们知道加锁确实能让线程安全,但是同时也付出了代价,一旦在一个线程中加了锁之后,那么就和运行高效无关了。(程序的速度就变慢了)因为加锁之后,线程之间是串行执行的。代码的运行效率就变慢了。
博主以前说过开发效率要比运行效率更重要,一切都要从程序员的利益出发,但是运行效率也不是说不重要!如果说运行效率不重要的话,那么我们在前面学习那么多的数据结构干啥,不都是使用一个较好的数据结构,来组织数据,让代码变得有效嘛
改进方案:
在instance初始化之前,才进行加锁,在初始化之后,不进行加锁。在加锁这里在加一个条件判断即可
代码如下:
//实现一个线程安全的单例模
class Singleton2{
//懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
public static Singleton2 instance = null;
private Singleton2(){}
public static Singleton2 getInstance(){
if(instance == null) { //如果instance被初始化过了,那么就不必再进行加锁,直接返回这个实例即可
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
public class TestDemo3 {
public static void main(String[] args) {
Singleton2 singleton2 = Singleton2.getInstance();
}
}
我们在上述的代码中,可以看到在getInstance()方法中,使用了两个条件判断语句,都是判断instance==null ,但是这两个条件判断语句的实际含义是千差万别。这两个添加判断长得一样,纯属是一个美丽的错误。
上面的条件判断是是否需要加锁。也就是说现在的instance是否已经被初始化过了
下面的条件判断是是否需要创建实例。
我们如果去掉了里层的条件判断语句那么就会变成:
//实现一个线程安全的单例模 class Singleton2{ //懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建 public static Singleton2 instance = null; private Singleton2(){} public static Singleton2 getInstance(){ if(instance == null) { synchronized (Singleton2.class) { //其实在这里加锁,就是加了个寂寞,在这里只针对设置实例加锁,在加锁语句的外面,还有istance == null 涉及到 读和判断,所以说加锁和没加一样,还是不符合原子性。 instance = new Singleton2(); } } return instance; } } public class TestDemo3 { public static void main(String[] args) { Singleton2 singleton2 = Singleton2.getInstance(); } }
如果直接对getInstance()方法进行加锁,那么就是一个无脑加锁
此处博主告诉各位老铁,在上述的代码中,还存在一个问题。但是已经线程安全了呀,哪里还有错呢?我们在单例模式中使用volatile,主要是使用volatile可以进制指令重排序,从而保证程序的正常运行。
public class Singleton {
private Singleton() {}
// 使用 volatile 禁止指令重排序
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 2
}
}
}
return instance;
}
}
注意观察上述代码,我标记了第 ① 处和第 ② 处的两行代码。给私有变量加 volatile 主要是为了防止第 ② 处执行时,也就是“instance = new Singleton()”执行时的指令重排序的,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:
创建内存空间。
在内存空间中初始化对象 Singleton。
将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。
试想一下,如果不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。
总结一下:
实现一个线程安全的单例模式—针对懒汉模式
- 在正确的位置加锁
- 双重if判定
- volatile关键字