JavaEE初阶-多线程4

文章目录

  • 一、单例模式
    • 1.1 饿汉模式
    • 1.2 懒汉模式
  • 二、阻塞队列
    • 1.1 生产者消费者模型
      • 1.1.1 现实生活举例
      • 1.1.2 生产者消费模型的两个优势
        • 1.1.2.1 解耦合
        • 1.1.2.2 削峰填谷
    • 1.2 阻塞队列代码
      • 1.2.1 使用java标准库的阻塞队列实现生产者消费者模型
      • 1.2.2 实现自己的阻塞队列


一、单例模式

单例模式是一种经典的设计模式,指的是对于整个进程中的某个类,有且仅有一个对象。单例模式有两种写法,分别为饿汉模式和懒汉模式。

1.1 饿汉模式

代码如下:

package Thread;

class Singleton {
    public static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {


    }
}

public class Demo31 {

    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        System.out.println(s1 == s2);


    }
}

为什么叫饿汉模式,因为在这个单例类中,在类的加载的时候直接定义并且建立了一个实例对象,这就凸显了“饿”的思想。因为在类的初始化时就已经建立好一个对象了,所以后续如果在多线程的情况下使用getInstance方法就不会设计线程安全的问题,因为此时只是一个多线程读取同一个变量的问题。然后我们还发现,单例类中的构造函数被private修饰,这时为了避免在类外去实例化其它的对象,从而达到“单例”的效果。

1.2 懒汉模式

在计算机这个领域当中,“懒”往往不是个贬义词,懒代表着高效率,懒汉模式不是在类初始化时就直接创建实例,而是等到需要使用实例的时候才去创建,这样当不需要使用实例时就能省下创建实例的开销。
代码如下:

package Thread;

class Singleton1 {

    public static Singleton1 instance = null;

    public static Singleton1 getInstance() {
     	if (instance == null) { 
        	instance = new Singleton1();
     	}
        return instance;
    }

    private Singleton1() {

    }
}

public class Demo32 {

    public static void main(String[] args) {
        Singleton1 s1 = Singleton1.getInstance();
        Singleton1 s2 = Singleton1.getInstance();


        System.out.println(s1 == s2);

    }
}


上述代码是一个懒汉模式的简单代码,我们不难想到它是线程不安全的。因为在多线程的环境下去调用getInstance这个方法相当于在多线程的环境下来修改同一个变量,就会出现线程安全问题。
在这里插入图片描述
如图,如果两个线程以这样的方式执行代码,线程1执行到if后线程2立马也执行到if,然后线程1创建实例,线程2也跟着创建实例,此时进程中就创建了两个实例,出现了安全问题。不要意味多创建一个实例没什么大不了的,单例模式的应用场景如下:

例一:
比如你写的服务器要从硬盘上加载100G数据到内存中,要写一个类来封装以上的加载操作,并且写一些获取或处理数据的逻辑,这样的类就应该是单例的,一个实例就管理100G的数据,建立多个实例机器也吃不消。
例二:
服务器可能会涉及一些配置项,代码中也需要专门的类来管理这些配置,需要加载配置数据到内存以供其它代码使用。这样的类也应该是单例的。因为配置是唯一的,如果有多个实例,那应该以哪个为准?

因此多创建一个实例,可能这个实例会管理100G的数据,会造成很大开销。下面我们回归正题,既然有线程安全的问题,那么我们就要去解决,给代码加锁。代码修改如下:

package Thread;

class Singleton1 {

    public static Object locker = new Object();
    public static Singleton1 instance = null;

    public static Singleton1 getInstance() {
        synchronized (locker) {
       		 if (instance == null) { //避免在多线程情况下重复创建对象,造成线程安全问题
       			instance = new Singleton1();
        	}
        }

        return instance;
    }

    private Singleton1() {

    }
}

public class Demo32 {

    public static void main(String[] args) {
        Singleton1 s1 = Singleton1.getInstance();
        Singleton1 s2 = Singleton1.getInstance();


        System.out.println(s1 == s2);

    }
}

这样就能避免前面的问题。当线程1进入if此时线程2是不可以的,因为加锁了,线程2直接堵塞。但是当实例创建好之后代码中就不涉及线程安全问题了,就是多个线程去读一个变量,同时加锁又是一个重量级得操作会影响到代码执行的效率,所以我们给getInstance方法的代码的锁之外再加上一层判断语句,如果已经有实例对象了就直接返回对象即可,无需再去执行后面的操作。代码修改如下:

package Thread;

class Singleton1 {

    public static Object locker = new Object();
    public static volatile Singleton1 instance = null;

    public static Singleton1 getInstance() {

        if (instance == null) { //避免已经建立了对象重新上锁浪费性能,直接返回对象即可
            synchronized (locker) {
                if (instance == null) { //避免在多线程情况下重复创建对象,造成线程安全问题
                    instance = new Singleton1();
                }
            }
        }

        return instance;
    }

    private Singleton1() {

    }
}

public class Demo32 {

    public static void main(String[] args) {
        Singleton1 s1 = Singleton1.getInstance();
        Singleton1 s2 = Singleton1.getInstance();


        System.out.println(s1 == s2);

    }
}

此时完成懒汉单例模式代码编写。我们可以看到我们在instance变量声明时加上了volatile关键字,这是为了避免编译器优化策略中的内存可见性问题,避免在线程1中创建实例对象线程2中感知不到,但是这是很小概率是为了以防万一。另外加上volatile也可以避免另一种编译器优化策略即指令重排序造成的问题。

指令重排序:
编译器比较智能,会将从代码中得到的二进制指令序列的顺序进行调整从而提高效率,重排序的前提就是结果不会发生改变,这种策略在单线程的情况下当然没有问题,但是在多线程的情况下就可能会出现问题。

对于instance = new Singleton1();这段代码可以分为三步,第一步就是申请空间,第二步初始化空间,第三步是将空间的地址赋给instance这里的引用,本来是这样的执行顺序,但是经过编译器优化策略即指令重排序,执行顺序变为了一三二。
在这里插入图片描述

如图,如果经过指令重排序后指令执行顺序为一三二,那么在线程1完成第一步和第三步即申请完空间并且赋给instance引用后线程2开始执行,因为此时instance已经被赋值并非为null,所以后面会直接返回instance,但是此时的instance是未被初始化的空间,因此对其进行操作肯定会出错。为了避免这种指令重排序造成的线程安全问题,就在instance前加上volatile,其它变量也是一样。

单例模式补充扩展:
单例模式确保反射安全,即使使用反射也无法破坏单例模式特性。
单例模式确保序列化下安全,即使使用java标准库中的序列化特性也无法破坏单例特性。
对象转为二进制字符串->序列化
二进制字符串转为对象->反序列化

二、阻塞队列

相对于优先级队列和普通队列,阻塞队列是线程安全的并且带有阻塞功能。当队列为空时如果要执行出队列的操作,那么出队列操作就会阻塞直至队列不为空。当队列满的时候也是一样,会阻塞入队列的操作直至队列不为满。BlockingQueue这就是java标准库提供的阻塞队列的接口。
与阻塞队列相似的还有消息队列,消息会通过topic对数据进行归类,每个类别都是一个阻塞队列,指定topic,每个topic下的数据都是先进先出的。因为消息队列这样的数据结构太好用了,所以在实际开发中往往会将消息队列封装成单独的服务器程序,这样的服务器程序也被称为消息队列。消息队列在实际开发中经常用于实现生产者消费者模型。普通的阻塞队列也可以实现生产者消费者模型,主要是看场景,如果是在一个进程中,那么使用阻塞队列即可,如果是需要在分布式系统中实现生产者消费者模型,那么就需要消息队列。

1.1 生产者消费者模型

生产者消费者模型是用来解决问题的经典方案。

1.1.1 现实生活举例

在这里插入图片描述
如图右三个滑稽包饺子,滑稽A负责擀饺子皮,滑稽B和C负责包饺子,滑稽A将饺子皮擀好了放在中间的盘子上,然后滑稽B和C拿盘子上的饺子皮来包饺子。在这个过程中A就是生产者,B和C就是消费者,A生产数据,B使用数据,中间的盘子是一个阻塞队列,当盘子中为空时相当于队列为空,此时B和C就要堵塞,要等待盘子中有饺子皮。当盘子被饺子皮装满,此时A就要阻塞,不能再放入饺子皮了。

1.1.2 生产者消费模型的两个优势

1.1.2.1 解耦合

在这里插入图片描述
以上是一个很简单的示意图,A和B之间相互调用,那么A当中就需要包含和B相关的代码或逻辑,相同的B当中也需要包含和A相关的代码或逻辑,这样A和B之间就具有了一定的耦合,当修改A时,B也要跟着改变,当修改B时也是一样A也要跟着改变。
在这里插入图片描述
如图当我们引入消息队列后A就不需要去直接和B打交道,A以及B直接和消息队列进行交互,这样A和B之间的互相影响很小。当我们要多引入一个C时,也不需要让A以及B修改任何代码,直接让C和消息队列交互即可,这样就达到了解耦合的效果。

1.1.2.2 削峰填谷

客户端发来的请求,个数多少无法预知,遇到某些突发事件可能会导致客户端对服务器的请求数量激增。一般来说接收方的处理逻辑相对复杂,当需求突然变多,服务器可能处理不过来导致直接挂掉。

在这里插入图片描述
在正常情况下都是A接收到一次请求就发送一条请求给B,因为B的处理逻辑通常比A复杂,因此当请求过多消耗的资源超过机器的上限,B就会挂掉。如图加入消息队列后就将B给保护起来了,此时B不需要考虑请求有多少,它可以按照自己的节奏来处理。
显然加入阻塞队列也有缺点,处理的速度变慢了。因为多了一次周转也就是网络通信,对于要求响应速度非常高的场景是不适用的。

1.2 阻塞队列代码

java标准库中的阻塞队列接口及其对应的阻塞队列的类如下。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
其中需要注意的是LinkedBlockingQueue类是自动扩容的,因此只会再队列为空时对出队操作阻塞,不会阻塞入队操作。

1.2.1 使用java标准库的阻塞队列实现生产者消费者模型

代码如下:

package Thread;

import java.util.concurrent.*;

public class Demo34 {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue<Integer> blockingQueue=new ArrayBlockingQueue<>(10);

        Thread t2 = new Thread(() -> {
            int count = 1;
            while (true) {
                System.out.println("t2生产:" + count);
                try {
                    blockingQueue.put(count);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
            }
        });

        Thread t3 = new Thread(() -> {

            try {
                while (true) {
                    System.out.println("t3消费:" + blockingQueue.take());
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }


        });

       
        t2.start();
        Thread.sleep(1000);
        t3.start();
    }


}

这段代码因为设置了进程中的放入时间的间隔,所以每次生产者线程t2数据一生成就被t3线程消费掉了,代码执行的效果如下:
在这里插入图片描述

1.2.2 实现自己的阻塞队列

代码如下:

package Thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;

class MyArrayBlockingQueue {
    private volatile int head = 0;
    private volatile int tail = 0;
    private volatile int len = 0;
    private String[] blockQueue;

    private int size;

    public MyArrayBlockingQueue(int capacity) {

        blockQueue = new String[capacity];
        size = capacity;
    }


    public void put(String str) throws InterruptedException {
        synchronized (this) {
            //加入while是因为再次判断 因为interrupt也可以唤醒wait 所以要杜绝这种可能。
            while (len == size) {
                this.wait();// 这里处理异常使用了throws 如果这里被interrupt方法唤醒那么函数直接结束执行
            }

            blockQueue[tail] = str;
            tail++;

            if (tail >= blockQueue.length) {
                tail = 0;
            }
            len++;
            this.notify();
        }

    }

    public String take() throws InterruptedException {
        synchronized (this) {
            while (len == 0) {
                this.wait();
            }

            String ret = blockQueue[head];
            head++;
            if (head >= size) {
                head = 0;
            }
            len--;
            this.notify();
            return ret;
        }
    }
}

public class Demo35 {


    public static void main(String[] args) throws InterruptedException {
        MyArrayBlockingQueue myArrayBlockingQueue = new MyArrayBlockingQueue(1000);


        Thread t1 = new Thread(() -> {
            try {
                int count = 1;
                while (true) {
                    System.out.println("生产:" + count);
                    myArrayBlockingQueue.put(count + "");
                    count++;
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });

        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    System.out.println("消费:" + myArrayBlockingQueue.take());
                   // Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }


            }

        });


        t1.start();
        t2.start();
    }
}

这里代码中使用了一个数组来实现了一个循环的阻塞的队列,大部分逻辑和循环队列是相似的,但是有一些部分不一样。代码中给put和take函数中都加上锁,因为这里要达到阻塞的效果就需要使用wait使得线程进入waiting状态,wait必须要在锁中使用。当使用put方法发现队列已经满了线程就要进入waiting状态,此时这里的判断条件是while循环,因为wait可以使用interrupt方法唤醒,所以使用循环多次判断,当某个线程调用了take方法拿走了队列中的数据,之后会直接唤醒这里put方法中的wait,take方法的思路也和put方法中一致。然后代码中的变量都加上了volatile为了以防万一避免内存可见性以及指令重排序的问题。

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

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

相关文章

【Go语言初探】(一)、Linux开发环境建立

一、操作系统选择 选择在Windows 11主机上运行的CentOS 7 Linux 虚拟机&#xff0c;虚拟化平台为VMWare Workstation. 二、安装Go语言环境 访问Go语言官网&#xff0c;选择Linux版本下载&#xff1a; 解压&#xff1a; tar -xvf go1.22.3.linux-amd64.tar.gz检验安装结果&…

uniapp + vue3 使用axios

场景 uniapp自带的uni.request不太好用&#xff0c;也有可能是自己用axios用的太熟悉了&#xff0c;所以还是用axios趁手点&#xff0c;所以尝试在uniapp中使用axios。 操作 因为uniapp项目没有package.json&#xff0c;所以先在项目根目录下执行 npm init, 执行完毕后直接…

算法设计与分析 例题解答 解空间与搜索

1.请画出用回溯法解n3的0-1背包问题的解空间树和当三个物品的重量为{20, 15, 10}&#xff0c;价值为{20, 30, 25}&#xff0c;背包容量为25时搜索空间树。 答&#xff1a; 解空间树&#xff1a; 搜索空间树&#xff1a; 2. 考虑用分支限界解0-1背包问题 给定n种物品和一背包…

线路和绕组中的波过程(三)

本篇为本科课程《高电压工程基础》的笔记。 本篇为这一单元的第三篇笔记。上一篇传送门。 冲击电晕对线路上波过程的影响 实际中的导线存在电阻&#xff0c;而且还有对地电导&#xff0c;会消耗一部分能量。但是因为雷击所涉及的传输距离很短&#xff0c;所以几乎可以忽略这…

JS代码随想录(一):数组

代码随想录 一、数组理论基础 二、LeetCode 704. 二分查找 三、LeetCode 27. 移除元素 四、LeetCode 977.有序数组的平方 五、LeetCode 209.长度最小的子数组 六、LeetCode 59.螺旋矩阵II 七、数组总结 一、数组理论基础 数组是存放在连续内存空间上的相同类型数据的集合。 数组…

你真的会用 ChatGPT 吗?来看看这 4 个模式,让你的 AI 技能更上一层楼!(上)

一年半已经过去&#xff0c;ChatGPT 虽然风靡一时&#xff0c;但真正发挥其超过 30% 效能的人却寥寥无几。许多资深用户依然沿用传统的人机交互方式&#xff0c;认为只需要事无巨细地编写指令&#xff0c;让 AI 服从即可。大多数人并不了解这些生成式 AI 的某些不为人之的特性&…

第四百九十八回

文章目录 1. 概念介绍2. 使用方法2.1 固定样式2.2 自定义样式 3. 示例代码4. 内容总结 我们在上一章回中介绍了"GetMaterialApp组件"相关的内容&#xff0c;本章回中将介绍使用get显示SnackBar.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在介…

python自动化办公的代码

以下是一个简单的Python自动化办公代码示例&#xff0c;用于实现一些基本的自动化任务&#xff0c;例如打开文件、读取数据、写入数据和保存文件等。 python import os # 打开文件 def open_file(filename): try: file open(filename, r) data file.read() file.close() ret…

如果你的Google ads账号被暂停了怎么办?

Google Ads账号暂停可能会导致您的企业收入损失。但实际上&#xff0c;除了一些特殊情况外&#xff0c;Google Ads帐户暂停几乎不是永久性的。如果您的帐户已被暂停&#xff0c;有多种方法可以恢复该帐户。 废话不多说&#xff0c;下面为大家分享&#xff01; 一、谷歌广告帐户…

用于视频识别的快慢网络

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;用于视频识别的快慢网络1、文献摘要2、提出方法2.1、SlowFast模型2.2、SlowFast 提出思想 3、相关方法3.1、时空间卷积3.2、基于光…

安卓开发--按键跳转页面

安卓开发--按键跳转页面 前言1. 按键页面跳转1.1 新建布局文件1.2 衔接布局文件&#xff0c;新建Java class类文件1.3 衔接布局文件&#xff0c;修改AndroidManifest.xml文件1.4 调用布局文件1.5 最终效果 前面已经介绍了一个空白按键工程的建立以及响应方式&#xff0c;可以参…

机器学习算法应用——神经网络回归任务、神经网络分类任务

神经网络回归任务&#xff08;4-3&#xff09; 神经网络回归任务&#xff0c;通常指的是使用神经网络模型进行回归分析。回归分析是一种统计学方法&#xff0c;用于研究一个或多个自变量&#xff08;预测变量&#xff09;与一个因变量&#xff08;响应变量&#xff09;之间的关…

python实现动态时钟功能

欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 一.前言 时钟,也被称为钟表,是一种用于测量、记录时间的仪器。时钟通常由时针、分针、秒针等计时仪器组成,是现代社会不可或缺的一种计时工具。它的发明和使用极大地改变了人类的生活方式和时间观念。 时钟的类型有很多,…

汇昌联信科技:做拼多多网店要押金吗?

做拼多多网店要押金吗?”这个问题&#xff0c;其实与拼多多的平台规则有关。在开店之前&#xff0c;商家需要详细了解平台的各项规定和费用构成&#xff0c;这样才能做好充足的准备。 一、明确回答问题 做拼多多网店&#xff0c;不需要支付押金。拼多多的入驻门槛相对较低&…

Threejs Shader动态修改Merge合并几何体中单个Mesh的颜色

目录 Merge合并 现象 思路 实现 为单个geometry添加映射 通过id检索Merge后的Geometry映射属性&#xff0c;获取顶点坐标 onBeforeCompile修改编译前材质的着色代码 编译前材质的顶点着色代码 编译前材质的片元着色代码 着色器代码 注意 效果 Merge合并 mergeBuf…

Vue路由拆分

1.在src下建立router&#xff0c;在router中建立文件index 2.将main.js中部分内容复制 App <template> <div><a href"#/friend">朋友</a><br><a href"#/info">信息</a><br><a href"#/music&quo…

数据结构十三:八大排序算法

排序算法&#xff08;sorting algorithm&#xff09;是用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用&#xff0c;因为有序数据通常能够被更高效地查找、分析和处理。排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定&am…

看马斯克与OpenAI的爱恨情仇,AGI之路会走向何方?

揭秘马斯克与OpenAI的决裂&#xff1a;AI的未来将何去何从&#xff1f; ©作者|Steven 来源|神州问学 引言 2024 年 3 月 1 日&#xff0c;时任OpenAI联合创始人的Elon Musk(下文简称&#xff1a;马斯克)将现任 CEO、创始人Sam Altman(下文简称&#xff1a;阿尔特曼)告上…

深度学习设计模式之单例模式

一、单例模式简介 一个类只能有一个实例&#xff0c;提供该实例的全局访问点&#xff1b; 二、单例模式实现步骤 使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。 私有构造函数保证了不能通过构造函数来创建对象实例&#xff0c;只能通过公有静态函数返…

工业机器人应用实践之玻璃涂胶(篇一)

工业机器人 工业机器人&#xff0c;即面向工业领域的机器人。工业机器人是广泛用于工业领域的多关节机械手或多自由度的机器装置&#xff0c;具有一定的自动性&#xff0c;可依靠自身的动力能源和控制能力实现各种工业加工制造功能。工业机器人被广泛应用于电子、物流、化工等…