【JavaEE精炼宝库】多线程(5)单例模式 | 指令重排序 | 阻塞队列

目录

一、单例模式:

1.1 饿汉模式:

1.2 懒汉模式:

1.2.1 线程安全的懒汉模式:

1.2.2 线程安全的懒汉模式的优化:

二、指令重排序

三、阻塞队列

3.1 阻塞队列的概念:

3.2 生产者消费者模型:

3.3 标准库中的阻塞队列:

3.4 阻塞队列实现:


一、单例模式:

单例模式是校招中最常考的设计模式之一

设计模式是什么?

设计模式好比象棋中的 "棋谱"。红方当头炮,黑方马来跳。针对红方的⼀些走法,黑方应招的时候有一些固定的套路。按照套路来走局势就不会吃亏。软件开发中也有很多常见的 "问题场景" 针对这些问题场景,大佬们总结出了一些固定的套路。按照这个套路来实现代码,也不会吃亏。大佬们为我们操碎了心。

单例模式能保证某个类在程序中只存在唯⼀⼀份实例,而不会创建出多个实例。这一点在很多场景上都需要。比如 JDBC 中的 DataSource 实例就只需要一个。

单例模式具体的实现方式有很多。最常见的是 "饿汉""懒汉" 两种。

1.1 饿汉模式:

类加载的同时,创建实例。

• 案例代码实现:

核心思想就是把构造方法设置为 private ,再把实例用 static 修饰。程序一运行,实例就被创建了,Singleton 类外面想要得到这个对象,只能通过 getInstance 来得到,所以能保证这个实例只被创建一次。

class Singleton{
    private static Singleton instance = new Singleton();//static 要记得加
    private Singleton(){}//这里要设置成 private,防止创建出多个实例
    public static Singleton getInstance(){
        return instance;
    }
}

1.2 懒汉模式:

类加载的时候不创建实例。第一次使用的时候才创建实例。

在计算机中 “懒” 是指高效的意思。这样如果后续这个类没有使用到,就可以把创建这个实例的损耗节省下来。

• 案例代码实现:

class SingletonLaze{
    private static SingletonLaze instance = null;
    private SingletonLaze(){}
    public static SingletonLaze getInstance(){
        if(instance == null){
            instance = new SingletonLaze();
        }
        return instance;
    }
}

到这里饿汉模式和懒汉模式的代码就已经大体编写完毕了。

请友友们思考一个问题:在多线程的情况下,上面的两种模式会出现线程不安全的情况嘛?

答:饿汉模式是线程安全的,懒汉模式是线程不安全的。

线程安全问题发生在首次创建实例时。如果在多个线程中同时调用 getInstance 方法,就可能导致创建出多个实例(虽然后续会被回收成一个,但是多个案例是实实在在被创建出来了,如果一个案例要使用 100G内存 ,会导致系统卡死的)。至于饿汉模式,在类加载的时候实例就已经被创建了,自然不存在线程安全问题。

1.2.1 线程安全的懒汉模式:

怎么解决懒汉模式的线程安全问题呢?

答:加锁。

加上 synchronized 可以改善这里的线程安全问题。

改进的案例代码如下:

class SingletonLaze {
    private static SingletonLaze instance = null;
    private static Object locker = new Object();

    private SingletonLaze() {
    }
    public static SingletonLaze getInstance() {
        synchronized (locker) {
            if(instance == null){
                //2  锁能不能加在 if 的里面
                instance = new SingletonLaze();
            }
            return instance;
        }
    }
}

这里友友们思考一下:锁能不能加在代码 2 的地方?

答:不能,如果多个线程同时进入的话,都能进入 if ,创建实例,那么锁就白加了。

到这里面试官可能还会问你,还能不能优化一下呢?

1.2.2 线程安全的懒汉模式的优化:

这里我们发现,只有在刚开始创建第一个实例的时候存在线程不安全的问题,创建完后,就和饿汉模式一样不会存在线程安全问题,这时代码还是一直加锁的话,会影响程序的效率,因为锁本身就是一个重量级操作。因此我们要在加锁的基础上,进一步改动。

• 使用双重 if 判定,降低锁竞争的频率。

• 给 instance 加上了 volatile。避免出现内存可见性导致的问题(这里概率很小)和指令重排序问题(大头)。

最终的代码如下:

class SingletonLaze {
    private static volatile SingletonLaze instance = null;
    private static Object locker = new Object();

    private SingletonLaze() {
    }

    public static SingletonLaze getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLaze();
                }
            }
        }
        return instance;
    }
}

在多线程中,许多在单线程看起来毫无意义的操作,在多线程就可能有不同的作用,只是代码的编写恰好相同而已。第一个 if 是为了判断要不要加锁,第二个 if 是为了判断要不要创建对象。

二、指令重排序

指令重排序也是编译器的一种优化策略。

我们写的代码最终编译成了一系列的二进制指令。正常来说,CPU 是按照顺序,一条一条执行的。但是编译器比较智能,会根据实际情况,生成的二进制指令的执行顺序和我们最初写的代码顺序可能会存在差别,调整顺序最主要的目的就是提高效率。(前提要保证逻辑是等价的)

就好比如:田忌赛马。不同的执行顺序,产生的结果是截然不同的。

单线程下编译器的指令重排序一般都是没有问题的,但是在多线程的情况下,编译器的判定就可能不是那么的准确了。

在懒汉模式的优化那里如果不加上 volatile 关键字(防止指令重排序)可能会发生什么事情呢? 

答: 

instance = new SingletonLaze();

这一行代码,大体上可以分为如下三个步骤:

1. 申请内存空间。

2. 调用构造方法。(对内存空间进行初始化)

3. 将此时内存空间的地址,赋值给 instance 引用。

在指令重排序的优化策略下,上述执行的过程可能是1,2,3。也可能是1,3,2。如果是1,3,2的话,在多线程的情况下,可能就会有 bug 。在多线程的情况下,如果在第一个抢到锁的线程,创建实例,执行完1,3 再到 2 的这个过程中,如果有新的线程进来,那么在最外层 if 判断时,就会认为 instance 已经有实例了,直接返回一个空引用,这时正好这个引用被进行 ' . ' 操作,就会出现 bug。 

上述谈到的指令重排序涉及到的 bug 是很难重现的,本身就是一个小概率事件。最好还是加上,如果出现问题,可能会带走年终奖😭。

三、阻塞队列

3.1 阻塞队列的概念:

队列我们已经很熟悉了。普通队列和优先级队列是线程不安全的。阻塞队列是一种特殊的队列。也遵守 "先进先出" 的原则。阻塞队列是一种线程安全的数据结构,并且具有以下特性:

• 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。

• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素。

阻塞队列的一个典型应用场景就是 "生产者消费者模型"。这是一种非常典型的开发模型。

3.2 生产者消费者模型:

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者,生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

生产者消费者模型,在开发中主要有两方面的意义:

• 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。(削峰填谷)

• 阻塞队列也能使生产者和消费者之间解耦。

3.3 标准库中的阻塞队列:

在 Java 标准库中内置了阻塞队列。如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可。

• BlockingQueue:接口

• ArrayBlockingQueue类:数组。

• LinkedBlockingQueue类 :链表。

• PriorityBlockingQueue类:堆 。

可以看到下面的三个类都实现了 BlockingQueue 接口。阻塞队列的使用方法如下:

• put 方法用于阻塞队列的入队列,take 用于阻塞队列的出队列(put、take 带有阻塞功能)。

• BlockingQueue 也有 offer, poll, peek 等方法,但是这些方法不带有阻塞特性。

案例演示:

import java.util.concurrent.*;
public class demo1 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);//设置阻塞队列的容量为 100
        Thread producer = new Thread(() -> {//生产者
            for (int i = 1; i < 100000; i++) {
                try {
                    System.out.println("生产:" + i);
                    queue.put(i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"生产者");
        producer.start();
        Thread customer = new Thread(() -> {//消费者
            for (int i = 1; i < 100000; i++) {
                try {
                    Thread.sleep(1000);
                    int tmp = queue.take();
                    System.out.println("消费:" + tmp);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者");
        customer.start();
    }
}

案例效果如下:

可以看到由于消费者被 sleep 了 1 秒,所以生产者马上就生产到了 100,到了 100 后由于阻塞队列具有阻塞功能,所以后续程序只能消费一个生产一个。

3.4 阻塞队列实现:

使用 synchronized 进行加锁控制。

要实现的功能有:

• put 插入元素的时候,判定如果队列满了,就进行 wait。(注意,要在循环中进行 wait。被唤醒时不一定队列就不满了,因为同时可能是唤醒了多个线程)。

• take 取出元素的时候,判定如果队列为空,就进行 wait 。(也是循环 wait)

具体的代码实现如下:

参数都在代码里面已经标好了,这里就不再赘述。唯一注意点就是在 wait 的条件语句使用 while 而不是 if。

public class MyBlockingQueue {
    private String[] elems = null;
    private volatile int tail = 0;//尾指针
    private volatile int head = 0;//头指针
    private volatile int size = 0;//大小

    public MyBlockingQueue(int capacity) {
        elems = new String[capacity];
    }
    /**
     * 把元素 elem 加入到队列中
     *
     * @param elem
     */
    public void put(String elem) throws InterruptedException {
        synchronized (this) {//保证线程安全
            while (size >= elems.length) {//最好写成 while
                //队列满的情况,阻塞
                this.wait();
            }
            //普通的队列操作
            elems[tail] = elem;
            size++;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }
            this.notify();//唤醒 take 
        }
    }

    /**
     * 从队列中取出 elem 元素
     * @return
     * @throws InterruptedException
     */
    //take
    public String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                //当队列为空时,阻塞
                this.wait();
            }
            String result = elems[head];
            size--;
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            this.notify();
            return result;//唤醒 put 
        }
    }
}

演示效果:

可以看到和上面使用标准库中的阻塞队列功能基本一致。

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/707745.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

公司面试题总结(五)

25.谈一谈箭头函数与普通函数的区别&#xff0c;箭头函数主要解决什么问题&#xff1f; 箭头函数与普通函数的区别&#xff1a; ⚫ 语法简洁性&#xff1a; ◼ 箭头函数使用>符号定义&#xff0c;省略了 function 关键字&#xff0c;使得语法更为紧凑。 ◼ 对于单行函…

剖析框架代码结构的系统方法(下)

当面对Dubbo、Spring Cloud、Mybatis等开源框架时,我们可以采用一定的系统性的方法来快速把握它们的代码结构。这些系统方法包括对架构演进过程、核心执行流程、基础架构组成和可扩展性设计等维度的讨论。 在上一讲中,我们已经讨论了架构演进过程和核心执行流程这两个系统方法…

【npm】console工具(含胶囊,表格,gif图片)

这是一款控制台花样输出工具 相对丰富的输出方式 文本输出属性值输出胶囊样式输出表格输出图片输出&#xff08;含动图&#xff09; 安装 npm install v_aot引用 import v_aot from "v_aot";字段说明 字段类型属性字符串值字符串类型default 、 primary 、 suc…

深入解析MySQL的层次化设计

一、基础架构 1.连接器 1.会先连接到这个数据库上&#xff0c;这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接 2.用户密码连接成功之后&#xff0c;会从权限表中拿出你的权限&#xff0c;后续操作权限都依赖于此时拿出的权限,这就意味着当链…

Github 2024-06-14 开源项目日报Top10

根据Github Trendings的统计,今日(2024-06-14统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量JavaScript项目2Python项目2非开发语言项目2TypeScript项目1Dart项目1Rust项目1Lua项目1Java项目1Jupyter Notebook项目1从零开始构建你喜爱的技…

代码随想录算法训练营第二十三天|669. 修剪二叉搜索树、 108.将有序数组转换为二叉搜索树、 538.把二叉搜索树转换为累加树

669. 修剪二叉搜索树 题目链接&#xff1a;669. 修剪二叉搜索树 文档讲解&#xff1a;代码随想录 状态&#xff1a;还可以 思路&#xff1a; 如果节点的值在[low, high]之间&#xff0c;则递归修剪它的左子树和右子树。 节点值小于low&#xff1a;如果节点的值小于low&#xff…

【机器学习】简答

1.什么是机器学习&#xff1f; 机器学习致力于研究如何通过计算的手段&#xff0c;利用经验来改善系统自身的性能。“训练”与“预测”是机器学习的两个过程&#xff0c;“模型”则是过程的中间输出结果&#xff0c;“训练”产生“模型”&#xff0c;“模型”指导 “预测”。计…

数字经济红利惠及全民,从掏钱消费到赚钱消费的转变,你准备好了吗?

伴随科技飞速发展&#xff0c;我们迎来了一个全新的经济时代——数字经济。数字经济以其独特的魅力&#xff0c;正为我们每个人带来前所未有的红利。 那么&#xff0c;面对数字经济的红利&#xff0c;我们是否已经做好了准备&#xff1f;我们又该如何把握这个时代赋予我们的机…

内存卡提示需要格式化?别急,这样拯救你的数据

一、内存卡突然提示需要格式化 在日常生活中&#xff0c;我们经常会使用到内存卡来存储照片、视频、文档等重要数据。然而&#xff0c;有时当我们试图访问内存卡时&#xff0c;却会遭遇一个令人头疼的问题——系统突然提示“内存卡需要格式化”。这意味着我们无法直接读取或写…

不愧是字节,图像算法面试真细致

这本面试宝典是一份专为大四、研三春招和研二暑假实习生准备的珍贵资料。 涵盖了图像算法领域的核心知识和常见面试题&#xff0c;包括卷积神经网络、实例分割算法、目标检测、图像处理等多个方面。不论你是初学者还是有经验的老手&#xff0c;都能从中找到实用的内容。 通过…

自动控制理论---零点和极点、单位脉冲响应

1、实验设备 PC计算机1台&#xff0c;MATLAB软件1套。 2、实验目的 研究四个具有相同极点分布但不同零点分布的二阶系统对单位脉冲响应的影响。绘制各系统的零点和极点分布图。计算并绘制各系统的单位脉冲响应波形。分析零点分布对单位脉冲响应的影响。 3、实验原理说明&am…

vue3和ant-design 实现前端多种验证密码规则,最全的前端验证密码规则

1、小眼睛可以显示/隐藏明文密码&#xff08;无法用input typepassword&#xff0c;用css样式实现切换明文&#xff09; 2、输入长度统计&#xff08;不是自带的&#xff0c;用div写的&#xff0c;然后定位到框内的&#xff09; 3、每输入一个字符分别验证每一项规则&#xf…

Talk|CVPR‘24 Oral:超越3D - Point Transformer V3中的多模态特征提取新构想

本期为TechBeat人工智能社区第599期线上Talk。 北京时间6月12日(周三)20:00&#xff0c;香港大学博士生—吴虓杨的Talk已经准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “超越3D - Point Transformer V3中的多模态特征提取新构想”&#xff0c;他通过P…

【React】Lodash---groupBy() 分组

例子 _.groupBy([6.1, 4.2, 6.3], Math.floor); // > { 4: [4.2], 6: [6.1, 6.3] }// The _.property iteratee shorthand. _.groupBy([one, two, three], length); // > { 3: [one, two], 5: [three] }思路分析 来源 定义一个名为groupBy的方法&#xff0c;通过扩展Ar…

同三维TT806-1 USB单路网络视频流/U盘采集卡

同三维TT806-1 USB单路网络视频流/U盘采集卡 (1路网络音视频信号或U盘直播推流器) 支持采集1路网络视频流或U盘音视频信号&#xff0c;USB输出到电脑 同时还可流推2个直播平台&#xff0c;可设置6组定时推流&#xff0c;有线网络 可录像到U盘&#xff0c;支持定时录像 一…

期末测试2--函数题---指针链表如何输出?

总结写代码时候遇到的问题 1.遍历指针链表 指针head在做for循环遍历的时候 for&#xff08;head, head!NULL;head&#xff09; head不能 for(head,head!NULL;headhead->next)-------正确的写法 int i; for(ihead;head!NULL;headhead->next) i 是 int 类型的&#x…

【思维导图工具】Xmind 2024安装教程+软件安装包下载

​XMind 2022是一款风靡全宇宙的思维导图和头脑暴炸软件&#xff0c;是全宇宙领先的“可视化思考”工具&#xff0c;每一个功能都能帮助你激发灵感、提高创造力。 XMind 2022为不同的使用场景提供多种可视化布局&#xff0c;让你的思维可以更清晰的结构化呈现&#xff0c;该软件…

【Linux】基础指令(一)

一、ls指令 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息 常见选项&#xff1a; -a 列出目录下的所有文件&#xff0c;包括以 . 开头的隐含文件。 -d …

WinForm之TCP客户端通讯

目录 一 设计界面 二 后台代码 一 设计界面 二 后台代码 using System.Net.Sockets; using System.Text;namespace TCP网络客户端通讯 {public partial class Form1 : Form{public Form1(){InitializeComponent();}TcpClient tcpClient new TcpClient();private void conne…

【STM32进阶笔记】GPIO端口

前段时间由于其他原因&#xff0c;专栏暂停更新了较长一段时间&#xff0c;现在恢复更新&#xff0c;争取继续为大家创造有价值的内容&#xff0c;期待大家的订阅关注&#xff0c;欢迎互相学习交流。 在STM32速成笔记系列专栏中其实已经对GPIO的一些必要知识进行了介绍&#xf…