IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 【多线程经典实例】实现一个线程安全的单例模式 -> 正文阅读

[Java知识库]【多线程经典实例】实现一个线程安全的单例模式

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种。

  1. 线程抢占式执行,线程间的调度充满了随机性。
  2. 多线程对一个变量进行修改。
  3. 针对变量操作不是原子的
  4. 内存可见性问题
  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()方法进行加锁,那么就是一个无脑加锁

但是博主告诉大家在上述加了if条件判断的代码中,还有一处问题。

我们可以想想,在多线程执行中,多个线程同时调用单例模式中的getInstance()方法,就会有大量的读instance内存的操作。

可能会让编译器把这个读内存操作优化成读寄存器操作

一旦这里触发了优化,后续如果第一个线程已经完成了对instance的修改,那么紧接着后面的线程都感知不到这个修改,仍然把instance当成null.

内存可见性问题,可能会引起第一个if判定失败,但是对于第二个if判定影响不大,因为synchronized本身就能保证内存可见性。只是引起第一层的误判,也就是导致,不该加锁的时候加锁了,但是不会影响第二层if的判断(不至于说创建多个实例)

解决内存可见性问题,给instance加上volatile关键字

//实现一个线程安全的单例模
class Singleton2{
    //懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
    public static volatile Singleton2 instance = null;
    private Singleton2(){}
    public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class) {
                    instance = new Singleton2();
            }
        }
        return instance;
    }
}
public class TestDemo3 {
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.getInstance();
    }
}

总结一下:

实现一个线程安全的单例模式—针对懒汉模式

  1. 在正确的位置加锁
  2. 双重if判定
  3. volatile关键字
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-10 11:43:27  更:2022-05-10 11:47:47 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 22:49:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码