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

news2024/12/25 0:44:40

目录

一、单例模式:

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 
        }
    }
}

演示效果:

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

结语:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络之网络层知识总结

网络层功能概述 主要任务 主要任务是把分组从源端传到目的端&#xff0c;为分组交换网上的不同主机提供通信服务。网络层传输单位是数据报。 分组和数据报的关系&#xff1a;把数据报进行切割之后&#xff0c;就是分组。 主要功能&#xff1a; 路由选择与分组转发 路由器…

ResNet——Deep Residual Learning for Image Recognition(论文阅读)

1.什么是ResNet ResNet是一种残差网络&#xff0c;咱们可以把它理解为一个子网络&#xff0c;这个子网络经过堆叠可以构成一个很深的网络。下面是ResNet的结构。 2.为什么要引入ResNet 理论上来说&#xff0c;堆叠神经网络的层数应该可以提升模型的精度。但是现实中真的是这…

SwiftUI中UIViewRepresentable的使用(UIKit与SwiftUI的桥梁)

UIViewRepresentable是一个协议&#xff0c;用于创建一个SwiftUI视图&#xff0c;该视图包装了一个UIKit视图。通过实现UIViewRepresentable协议&#xff0c;我们可以在SwiftUI中使用自定义的UIKit视图&#xff0c;并与SwiftUI进行交互。 实现UIViewRepresentable 创建一个遵…

DT浏览器很好用

简单的浏览器&#xff0c;又是强大的浏览器&#xff0c;界面简洁大方&#xff0c;操作起来非常流畅&#x1f60e;&#xff0c;几乎不会有卡顿的情况。 搜索功能也十分强大&#x1f44d;&#xff0c;能够快速精准地找到想要的信息。 而且还有出色的兼容性&#xff0c;各种网页都…

qt 实现模拟实际物体带速度的移动(水平、垂直、斜角度)——————附带完整代码

文章目录 0 效果1 原理1.1 图片旋转1.2 物体带速度移动 2 完整实现2.1 将车辆按钮封装为一个类&#xff1a;2.2 调用方法 3 完整代码参考 0 效果 实现后的效果如下 可以显示属性&#xff08;继承自QToolButton&#xff09;: 鼠标悬浮显示文字 按钮显示文字 1 原理 类继承…

单链表经典算法题 1

前言 学习了单链表&#xff0c;我们就做一些题来巩固一下。还有就是解题方法不唯一&#xff0c;我就只讲述为自己的方法。 目录 前言 1.移除链表元素 思路 代码 2.反转链表 思路 代码 3.链表的中间节点 思路 代码 总结 1.移除链表元素 思路 我们创建一个新的表…

FM全网自动采集聚合影视搜索源码

源码介绍 FM 全网聚合影视搜索(响应式布局)&#xff0c;基于 TP5.1 开发的聚合影视搜索程序&#xff0c;本程序无数据库&#xff0c;本程序内置P2P 版播放器&#xff0c;承诺无广告无捆绑。片源内部滚动广告与本站无关,谨防上当受骗&#xff0c;资源搜索全部来自于网络。 环境…

Java面向对象之static关键字,可变参数,递归,数组常见算法,对象数组,方法参数

第一章.static关键字 1.static的介绍以及基本使用 1.概述:static是一个静态关键字 2.使用:a.修饰一个成员变量:static 数据类型 变量名b.修饰一个方法:修饰符 static 返回值类型 方法名(形参){方法体return 结果}3.调用静态成员:类名直接调用(不用new对象)4.静态成员特点:a.静…

智慧守护 畅游无忧——北斗应急呼叫柱,为景区安全加码

在大自然的怀抱中&#xff0c;中型及大型公园、景区以其壮丽风光吸引着成千上万的游客前来探索&#xff0c;成为了人们休闲娱乐的好去处。然而&#xff0c;广袤的区域、复杂的地形和分散的人流也给安全保障带来了前所未有的挑战。传统的巡逻方式难以覆盖每一个角落&#xff0c;…

2.nginx常用命令

使用nginx命令需要进入nginx目录里面执行。 /usr/local/nginx/sbin/ 查看nginx的版本号 启动nginx ./nginx 关闭nginx ./nginx -s stop 查看nginx的是否运行的命令 重新加载nginx 针对配置目录中配置文件nginx.cnf修改后需要重新加载 /usr/local/nginx/conf/nginx.cnf …

WebSocket 详解--spring boot简单使用案例

一、什么是WebSocket WebSocket 是一种网络通信协议&#xff0c;专为在单个 TCP 连接上进行全双工通信而设计。WebSocket 允许客户端和服务器之间的消息能够实时双向传输。这与传统的 HTTP 请求-响应模式有很大的不同。 二、WebSocket 的关键特性 双向通信&#xff1a;WebSocke…

vi/vim使用命令

你是否在编辑文件时以为键盘坏了&#xff0c;为什么不能删除呢&#xff0c;为什么不能敲代码呢&#xff0c;等你初识vi&#xff0c;会觉得这个东西为什么设计得这么难用&#xff0c;这篇教程带你熟练得用上这款经典的工具 Vi 是在 Unix 系统上广泛使用的编辑器&#xff0c;Vim …

java原子变量

在Java中&#xff0c;原子变量是一种特殊的变量&#xff0c;它们提供了一种不需要显式加锁的情况下进行线程安全的操作。Java.util.concurrent.atomic包提供了原子变量类&#xff0c;如AtomicInteger&#xff0c;AtomicLong等&#xff0c;它们利用底层硬件的原子操作来保证线程…

VRChat 2024年裁员原因与背景深度分析

VRChat&#xff0c;作为2022年元宇宙/VR社交领域的巨头&#xff0c;近期在2024年宣布裁员计划&#xff0c;其背后原因和背景值得业界尤其是仍在纯元宇宙虚拟空间创业的同仁们重点关注。 一、创始人决策失误 根据CEO的邮件披露&#xff0c;VRChat的创始人因缺乏经验和过度自信…

HTTP 概述

HTTP 概述 HTTP 是一种用于获取资源&#xff08;如 HTML 文档&#xff09;的协议。 它是 Web 上任何数据交换的基础&#xff0c;它是一种客户端-服务器协议&#xff0c;这意味着请求由接收方&#xff08;通常是 Web 浏览器&#xff09;发起。 一个完整的文档是从获取的不同子文…

10 SpringBoot 静态资源访问

我们在开发Web项目的时候&#xff0c;往往会有很多静态资源&#xff0c;如html、图片、css等。那如何向前端返回静态资源呢&#xff1f; 以前做过web开发的同学应该知道&#xff0c;我们以前创建的web工程下面会有一个webapp的目录&#xff0c;我们只要把静态资源放在该目录下…

N32G45XVL-STB之移植LVGL(8.4.0)

目录 概述 1 系统软硬件 1.1 软件版本信息 1.2 ST7796-LCD 1.3 MCU IO与LCD PIN对应关系 2 认识LVGL 2.1 LVGL官网 2.2 下载V8.4.0 3 移植LVGL 3.1 硬件驱动实现 3.2 添加LVGL库文件 3.3 移植和硬件相关的代码 3.3.1 驱动接口相关文件介绍 3.3.2 重新接口函数 3…

Lecture3——线性最优化(Linear Optimization)

一&#xff0c;本文重点 线性最优化&#xff08;LP&#xff09;和标准线性最优化&#xff08;Standard LP form&#xff09;的定义如何将LP转换为Standard LP用Python解决LP问题将非线性最优化问题&#xff08;NLP&#xff09;转换为LP 二&#xff0c;定义 1&#xff0c;线性…

Java多线程面试重点-1

0. 什么是并发&#xff1f;什么是并行&#xff1f; 并发&#xff1a;把时间分成一段一段&#xff0c;每个线程轮流抢占时间段。 如果时间段非常短&#xff0c;线程切换非常快&#xff0c;被称为伪并行。并行&#xff1a;多个线程可以同时运行。 并发与并行造成的影响&#xff…

k8s之kubelet证书时间过期升级

1.查看当前证书时间 # kubeadm alpha certs renew kubelet Kubeadm experimental sub-commands kubeadm是一个用于引导Kubernetes集群的工具&#xff0c;它提供了许多命令和子命令来管理集群的一生周期。过去&#xff0c;某些功能被标记为实验性的&#xff0c;并通过kubeadm a…