JavaEE初阶Day 10:多线程(8)

news2024/12/25 9:44:24

目录

  • Day 10:多线程(8)
    • 单例模式
    • 阻塞队列
      • 1. 生产者消费者模型
        • 1.1 生产者消费者模型解耦合
        • 1.2 生产者消费者模型削峰填谷
      • 2. 生产者消费者代码
      • 3. 阻塞队列实现

Day 10:多线程(8)

单例模式

单例模式:某个类在进程中只能有唯一实例,需要一定的编程技巧,作出限制,一旦代码写的有问题,创建了多个实例,直接编译报错

  • 饿汉模式:程序运行的时候,就立即创建实例

  • 懒汉模式:首次使用的时候,才创建实例

    • 加锁:把if和new包裹起来

    • 双重if

    • 给变量上加上volatile

      可能会涉及到内存可见性问题:t1线程修改了Instance引用,t2有可能读不到(概率应该比较小),加上volatile是为了万无一失,另一方面,加上volatile也能够解决指令重排序引起的线程安全问题

指令重排序:也是编译器的一种优化策略,编译器优化有很多种策略,比如把读内存优化到读寄存器、指令重排序、循环展开、条件分支预测等

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

  • 指令重排序的前提一定是重新排序之后,逻辑和之前等价
  • 单线程下,编译器进行指令重排序的操作,一般都是没有问题的,编译器可以准确地识别出,哪些操作可以重排序,而不会影响到逻辑
  • 多线程下,判定就可能不准确了,可能出现重排序后,逻辑发生了改变

对于instance = new SingletonLazy();可以大体上细分为三个步骤:

  1. 申请内存空间
  2. 调用构造方法(对内存空间进行初始化)
  3. 把此时内存空间的地址,赋值给instance引用

在指令重排序优化策略下,上述执行的过程,不一定是123,有可能是132(1一定是先执行的),这两种执行方式,单线程下都是可以的,但是如果是132,在多线程下,可能会引起bug

package thread;


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

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

    private SingletonLazy() {

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}	
  • t1线程判断instance == null成立,进行加锁,进一步判断instance == null成立,进行instance = new SingletonLazy(),在这一过程中完成了1申请内存和3把地址赋值给引用,一旦3执行完,意味着instance为非null,但是指向的对象其实是一个未初始化的对象(里面的成员都是默认值)
  • 此时t2线程判断instance == null不成立,直接返回instance这个未初始化完毕的对象
  • 然后接下来t1线程才开始进行2调用构造函数

这种情况下,后续的对SingletonLazy s1 = SingletonLazy.getInstance();操作,都是针对未初始化的对象进行操作,存在严重问题

要解决上述问题,就需要引入volatile

  • volatile不仅仅能解决内存可见性问题,也能禁止针对这个变量读写操作的指令重排序问题
  • 指令重排序在很多地方都可能发生,volatile特指的是针对某个对象的读写操作过程中,不会出现重排序
  • 按照加上volatile之后,此时t2线程读到的数据,一定是t1已经构造完毕的完整对象了

上述谈到的指令重排序涉及到的问题很难进行验证,本身就是一个小概率的事件,即使不加volatile运行程序,运行几百次几千次,应该也是正确的,指不定啥时候会出现问题,加上volatile总是万无一失的做法,程序员也不确定是否在某个JVM这样版本中更好的处理这样的问题

面试中考察单例模式

  1. 先写最初的版本,即不考虑线程安全的版本
  2. 加上锁
  3. 加上双重if
  4. 最后加上volatile

关于单例模式的延伸

(1)单例模式要确保反射下安全,即使动用反射也无法破坏单例特性

(2)单例模式要确保序列化下安全,即使动用Java标准库的序列化机制,也无法破坏单例特性

阻塞队列

之前学习过的普通队列和优先级队列都是线程不安全的,阻塞队列是先进先出的、线程安全的并且带有阻塞功能

  • 队列为空,尝试出队列,出队列操作就会阻塞,一直阻塞到队列不为空为止
  • 队列为满,尝试入队列,入队列操作也会阻塞,一直阻塞到队列不满为止

BlockingQueue就是标准库提供的阻塞队列

除了阻塞队列之外,还有消息队列:不是普通的先进先出,而是通过topic这样的参数来对数据进行归类,出队列的时候,指定topic,每个topic下的数据是先进先出的,消息队列往往也会带有阻塞特性

由于消息队列这样的数据结构太好用了,因此实际开发中,经常会把这样的数据结构封装成单独的服务器程序,单独部署

消息队列能够起到的作用,就是实现“生产者消费者模型”

1. 生产者消费者模型

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

  • 能够让程序进行解耦合
  • 能够使程序削峰填谷

生产者消费者模型的实现:

  • 需要在一个进程内实现,使用阻塞队列即可
  • 需要在分布式系统中实现,需要使用单独部署的消息队列服务器

简单来说生产者消费者模型就是一些线程负责“生产产品”,另一些线程负责“消费产品”

如果“生产产品”速度较慢,那么“消费产品”就会阻塞等待

如果“消费产品”速度较慢,那么“生产产品”就会阻塞等待

也就是说生产者和消费者之间多了一个消息队列

1.1 生产者消费者模型解耦合

在这里插入图片描述

如果让A直接调用B,意味着A的代码中就要包含很多和B相关的逻辑,B的代码中也会包含和A相关的逻辑,彼此之间就有一定的耦合

  • 一旦A做出了修改,可能就会影响到B,反之亦然
  • 一旦A出现了BUG,也容易把B牵连到,反之亦然

在这里插入图片描述

然而在引入了消息队列之后:

  • 站在A的视角,不知道B的存在,只关心和队列的交互
  • 站在B的视角,不知道A的存在,只关心和队列的交互
  • 此时,对A的修改就不太容易影响到B,A如果挂了,也不会影响到B,反之亦然
  • 未来如果再引入C,也让A访问C,A不需要修改任何代码,直接让C从队列里读取数据即可,提升了程序的可扩展能力
1.2 生产者消费者模型削峰填谷

客户端发来的请求,个数多少,没办法提前预知,遇到某些突发情况,就可能会导致客户端给服务的请求激增

在这里插入图片描述

正常情况下,A收到一个客户端的请求,就同样要请求一次B,A收到的请求激增了,B的请求也会激增,但是由于A做的工作比较简单,消耗的资源少,B做的工作更复杂,消耗的资源多,一旦请求量大了,B就容易挂,所以引入消息队列

  • 无论A给队列写的多快,B都可以按照固有的节奏来消费数据
  • B的节奏,就不一定完全跟着A了,相当于队列把B保护起来了
  • B要进行很多重量级操作,比如操作数据库之类的,要消耗很多系统资源花费一定的时间
  • 消息队列没有什么业务逻辑,消耗的硬件资源少,本身就抗造,同时,实际开发中,部署消息队列的机器一般都会给配置比较高的机器/集群

引入消息队列来实现生产者消费者模型,效率是不如直接访问来得更快的,多了一次周转,也多了一次网络通信

2. 生产者消费者代码

package thread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Demo29 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
        queue.put("A");
        String elem = queue.take();
        System.out.println("elem = " + elem);
        elem = queue.take();
        System.out.println("elem = " + elem);

    }
}
package thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Demo30 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1000);

        Thread t1 = new Thread(() ->{
            try {
                while (true){
                    Integer value = queue.take();
                    System.out.println("t1 消费:" + value);
                    Thread.sleep(1000);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });

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

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

3. 阻塞队列实现

package thread;

class MyBlockingQueue {
    private String[] elems = null;
    //[head, tail)
    //head位置指向的是第一个元素,tail指向的是最后一个元素的下一个元素
    private volatile int head = 0;
    private volatile int tail = 0;
    private volatile int size = 0;

    public MyBlockingQueue(int capacity){
        elems = new String[capacity];
    }

    void put(String elem) throws InterruptedException {
        synchronized (this) {
            while (size >= elems.length){
                //队列满了,进行队列阻塞
                this.wait();
            }
            //把新的元素放到tail所在的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                //到达末尾,就回到开头
                tail = 0;
            }
            //更新size的值
            size++;


            //唤醒下面 take 阻塞的wait
            this.notify();
        }


    }

    String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                //队列空了,进行阻塞
                this.wait();
            }
            //取出 head 指向的元素
            String result = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }

            size--;
            //take 成功一个元素,就唤醒上面put中的wait操作
            this.notify();
            return result;
        }
    }
}

public class Demo31 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);

        Thread t1 = new Thread(() -> {
            try {
                int count = 1;
                while (true) {
                    queue.put(count + "");
                    System.out.println("生产" + count);
                    count++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                while (true){
                    String result = queue.take();
                    System.out.println("消费" + result);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });

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

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

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

相关文章

2024经常用且免费的10个网盘对比,看看哪个比较好用!

网盘在我们的工作和学习中经常会用到&#xff0c;也是存储资料的必备工具&#xff0c;有了它&#xff0c;我们就不用走到哪都带着移动硬盘了&#xff0c;而目前市场上的主流网盘还有数十款&#xff0c;其中有免费的也有付费的&#xff0c;各家不一&#xff0c;今天小编就来为您…

嵌入式操作系统FreeRTOS(队列管理)

1.队列管理 &#xff08;1&#xff09;数据存储 队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。通常情况下&#xff0c;队列被作为FIFO (先进先出)使用&#xff0c;即数据由队列尾…

Win10安装DeepSpeed 实测成功

I. 完整流程 按照DeepSpeed要求步骤安装&#xff0c;即 Windows Windows support is partially supported with DeepSpeed. On Windows you can build wheel with following steps, currently only inference mode is supported. Install pytorch, such as pytorch 1.8 cu…

Tomcat和Spring Boot配置https

生成测试证书 生成证书前&#xff0c;先验证本地是否正确配置jdk环境变量&#xff0c;如果jdk环境变量配置正确&#xff0c;在命令行程序输入生成证书的命令。 keytool -genkey -alias tomcat -keyalg RSA -keystore "F:\job\apache-tomcat-8.5.29\key\freeHttps.keysto…

goland2024安装包(亲测可用)

目录 一、软件简介 二、软件下载 一、软件简介 Goland 是一款由 JetBrains 公司开发的集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门用于 Go 语言的开发。它提供了丰富的功能和工具&#xff0c;帮助开发者更高效地编写、调试和管理 Go 语言项目。 功能特点&#x…

OpenHarmony实战开发-如何实现进入页面,点击动画卡片,动画播放并且文本发生变化。

介绍 Lottie是一个适用于OpenHarmony的动画库&#xff0c;它可以解析Adobe After Effects软件通过Bodymovin插件导出的json格式的动画&#xff0c;并在移动设备上进行本地渲染&#xff0c; 可以在各种屏幕尺寸和分辨率上呈现&#xff0c;并且支持动画的交互性&#xff0c;通过…

设备基础命令,路由基础

直连路由 静态路由 动态路由 根据路由器学习路由信息、生成并维护路由表的方法包括直连路由(Direct)、静态路由(Static)和动态路由(Dynamic)。直连路由&#xff1a;路由器接口所连接的子网的路由方式称为直连路由&#xff1b;非直连路由&#xff1a;通过路由协议从别的路由器…

【机器学习300问】75、如何理解深度学习中Dropout正则化技术?

一、Dropout正则化的原理是什么&#xff1f; Dropout&#xff08;随机失活&#xff09;正则化是一种用于减少神经网络中过拟合现象的技术。Dropout正则化的做法是&#xff1a; 在训练过程中的每次迭代中&#xff0c;随机将网络中的一部分权重临时"丢弃"&#xff08;即…

AndroidStudio AGP 7+, 编译aar并输出到本地仓库

1 编写构建gradle脚本代码 1.1 配置publication和repository 在指定moudle目录下新建名为"maven-publish.gradle"文件&#xff0c;其声明的publication和repository如下所示&#xff1a; apply plugin: maven-publish// This creates a task called publishReleas…

线性表的链式存储(循环链表)

文章目录 前言一、循环链表是什么&#xff1f;二、循环链表的操作实现总结 前言 T_T此专栏用于记录数据结构及算法的&#xff08;痛苦&#xff09;学习历程&#xff0c;便于日后复习&#xff08;这种事情不要啊&#xff09;。所用教材为《数据结构 C语言版 第2版》严蔚敏。有关…

电商数据采集的5种方法|电商数据采集|电商数据分析|电商API接口

电商数据采集有5种方式&#xff0c;包括API、RPA、数据库连接、Excel下载和ERP等业务系统数据采集。这些方法可帮助卖家获取多平台电商数据&#xff0c;进行深度挖掘&#xff0c;实现电商运营的优化。 电商竞争白热化的今天&#xff0c;一个电商卖家往往会在多个平台铺设店铺来…

sora related

官方https://openai.com/research/video-generation-models-as-world-simulators 概述&#xff1a; sora可以生成变长的、不同分辨率的最长可到1分钟的视频&#xff1b;整体流程是 v i d e o c o m p r e s s i o n n e r w o r k ( v i d e o → l a t e n t ) p a t c h i…

windows下安装kibana

下载&#xff1a;https://www.elastic.co/cn/downloads/kibana 安装&#xff1a;https://www.elastic.co/guide/cn/kibana/current/install.html 安装好后&#xff0c;cd到kibana的bin目录&#xff0c;启动kibana.bat 然后访问localhost:5601

C++入门 (2)

文章目录 C入门C输入输出缺省参数全缺省半缺省函数声明与定义分离 函数重载C支持函数重载的原理--名字修饰 C入门 C输入输出 C输入输出包含在# include《iostream》中 cout 类似在控制台中输出&#xff0c;使用cout需要使用流插入符&#xff08;<<&#xff09; 这个符号…

YAML教程-1-基础入门

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go YAML简介 YAML&#xff08;YAML Aint Markup Language&#xff09;是一种用于数据序列化的人类可读格式。它广泛用于配置文件、数据交换、持续集成/持续部署&#xff08;CI/CD&#xff09;等领域。YAML的设计目标…

基于springboot实现电影评论网站系统设计项目【项目源码+论文说明】

基于springboot实现电影评论网站系统演示 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了电影评论网站的开发全过程。通过分析电影评论网站管理的不足&#xff0c;创建了一个计算机管理电影评论网站的方案。文…

采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示

采用C#.Net JavaScript 开发的云LIS系统源码 二级医院应用案例有演示 一、系统简介 云LIS是为区域医疗提供临床实验室信息服务的计算机应用程序&#xff0c;可协助区域内所有临床实验室相互协调并完成日常检验工作&#xff0c;对区域内的检验数据进行集中管理和共享&#xff0…

继电器会不会被淘汰?

继电器作为一种电控制器件&#xff0c;其基本功能是在输入量达到一定条件时&#xff0c;使电气输出电路中的被控量发生预定的阶跃变化。 尽管现代电子技术发展迅速&#xff0c;新型产品不断涌现&#xff0c;但继电器因其独特的优势在许多应用领域仍然不可替代。 技术优势&#…

【Axure教程】制作书本翻页效果

翻书效果是一种模拟真实书本翻页动作的视觉效果&#xff0c;常用于网页设计和应用程序中&#xff0c;以增强用户体验和交互性。这种效果通常通过动画和过渡效果来模拟书页的翻转&#xff0c;使用户感觉像在真实的书本中翻页一样。 所以今天作者就教大家怎么在Axure里用中继器制…

从API到Agent:洞悉LangChain工程化设计

作者&#xff1a;范志东 原文&#xff1a;https://mp.weixin.qq.com/s/zGS9N92R6dsc9Jk57pmYSg 本文作者试着从工程角度去理解LangChain的设计和使用。大家可以将此文档作为LangChain的“10分钟快速上手”手册&#xff0c;希望帮助需要的同学实现AI工程的Bootstrap。 我想做一…