【Java】多线程案例(单例模式,阻塞队列)

news2024/10/7 12:26:47
> :heart: Author:    老九

☕️ 个人博客:老九的CSDN博客
🙏 个人名言:不可控之事 乐观面对
😍 系列专栏:

文章目录

  • 实现安全版本的单例模式
    • 饿汉模式
      • 类和对象的概念
      • 类对象
      • 类的静态成员与实例成员
    • 懒汉模式
      • 如何保证懒汉模式的线程安全
  • 阻塞队列
    • 让多个服务器之间充分解耦
    • 能让请求进行 "削峰填谷"
      • 标准库中的阻塞队列
      • 自己实现阻塞队列

实现安全版本的单例模式

  • 单例模式是设计模式之一。代码当中的某个类,只能有一个实例,不能有多个。单例模式分为:饿汉模式和懒汉模式

饿汉模式

饿汉模式表示很着急,就想吃完饭剩下很多碗,然后一次性把碗全洗了。就是比较着急的去创建实例。用static来创建实例,利用在类加载时初始化,只有一份拷贝存在于内存中的特性实现单例模式,并且立即进行实例化。下面代码中的instance对应的实例,就是该类唯一的实例:

class Singleton{
    public static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

public class Example{
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance == instance2);
    }
}

为了防止程序员在其他地方不小心new这个Singleton,于是把构造方法设为private了

类和对象的概念

类是对象的模板,描述了对象的行为和状态。
对象是类的实例,它是在内存中分配的实体,具有实际的属性和行为。

类对象

在Java中,每个类在加载到内存后,都会有一个对应的类对象。这个类对象存储了类的相关信息,包括类的名称、方法、属性等。
类对象是Java虚拟机(JVM)在运行时对类的抽象表示。

类的静态成员与实例成员

静态成员(类属性/类方法)是与类关联的,而不是与类的实例相关联的。它们在类加载时初始化,并且只有一份拷贝存在于内存中,被所有类的实例共享。
实例成员(实例属性/实例方法)是与类的实例相关联的,每个类的实例都有自己的一份实例成员。

懒汉模式

懒汉模式主要是,不立即初始化实例,只有在被调用的时候,才会创建实例

如何保证懒汉模式的线程安全

加锁,把创建实例的代码加锁就可以了,加锁的时候,可以直接指定类对象.class作为锁对象。加锁之后,线程安全问题得到了解决,但是又有了新的问题。在多线程调用获取信息的时候,可能涉及到读和修改,但是一旦实例被初始化之后,就只剩读操作了。

class Singleton{
    private static volatile Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

public class Example{
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance == instance2);
    }
}

为什么要两次判断: 因为在并发环境中,多个线程可能会同时通过第一次检查,此时可能会出现多个线程都创建实例的情况。第二次检查可以确保只有一个线程能够创建实例,保证了单例模式的唯一性。
为什么需要加volatile: 因为如果有多个线程的话,都去读getInstance就可能导致内存可见性的问题,所以需要加上volatile来避免内存可见性问题
和饿汉模式的区别就是,懒汉模式只有在使用的时候,才会创建实例,饿汉模式在类加载的时候就会创建实例。

阻塞队列

队列的特性是先进先出,相对于普通队列,阻塞队列又有其他方面的功能:

  1. 线程安全
  2. 产生阻塞效果:
    a. 如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止。
    b. 如果队列为满,尝试入队列,就会出现阻塞,阻塞到队列不为满为止。

通过上面这种特性,就可以实现 “生产者消费者模型” 。就像我们烤串,有人烤,有人吃,然后烤好的放在烤盘上面。对于吃烤串来说,烤盘就是交易场所。此处的阻塞队列就可以作为生产者消费者模型当中的交易场所。

让多个服务器之间充分解耦

生产者消费者模型,是实际开发当中非常有用的一种多线程开发手段,尤其是在服务器开发场景当中。假设有两个服务器 A 和 B,A 作为入口服务器直接接受用户的网络请求,B 作为应用服务器,来给 A 提供一些数据。如图:
在这里插入图片描述
如果不使用生产者消费者模型,此时 A 和 B 的耦合性是比较强的。在开发 A 代码的时候,就得充分了解到 B 提供的一些接口,开发 B 代码的时候,也得充分了解到 A 是怎么调用的。一旦想把 B 换成 C ,A 的代码就需要较大的改动。而且如果 B 挂了,也可能直接导致 A 也顺带挂了。
使用生产者消费者模型,就可以降低这里的耦合,就像这样:
在这里插入图片描述

能让请求进行 “削峰填谷”

未使用生产者消费者模型的时候,如果请求量突然暴涨。A 暴涨=> B 暴涨,A 作为入口服务器,计算量较小,不会产生问题。B 作为应用服务器,计算量可能很大,需要的系统资源也更多,如果请求更大了,就可能导致程序挂了。如图:
在这里插入图片描述
如果使用阻塞队列的话,A 的请求暴涨 => 阻塞队列的请求暴涨,由于阻塞队列没啥计算量,只是存数据,所以抗压能力就更强。B 这边依然按照原来的速度进行处理数据,就不会受到 A 的暴涨。所以就不会引起崩溃。也就是 “削峰”。这种峰值很多时候不是持续的,过去之后就恢复了。B 仍然是按照原有的频率来处理之前积压的数据,就是 “填谷” 。
实际开发当中:阻塞队列不是一个简单的数据结构了,而是一个/一组专门的服务器程序,提供的功能不仅仅是队列阻塞。还会在这些基础上面提供更多的功能(数据持久化存储,多个数据通道,多节点备份,支持控制面板,方便配置参数),又叫”消息队列“。

标准库中的阻塞队列

通过 BlockingQueue 来实现阻塞队列,代码如下:

public class Example{
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> stringBlockingDeque = new LinkedBlockingDeque<>();
        //入队
        stringBlockingDeque.put("hello");
        //出队
        String s = stringBlockingDeque.take();
        System.out.println(s);
    }
}

自己实现阻塞队列

1.先实现一个普通队列(通过数组来实现)
2.再加上线程安全
3.再加上阻塞
实现一个普通队列:
在这里插入图片描述
出队列就是把 head 位置的元素返回去,并且 head++。当 tail 加满的时候,就回到队列头。所以重要的就是区别空队列和满队列。所以我们创建一个变量来记录元素的个数:size == 0 就是空,size == arr.length 就是满。
保证线程安全:
1.在多线程环境下,使用入队和出队没有问题。
2.入队和出队的代码是属于公共操作变量,所有给整个方法加锁。
实现阻塞效果:
通过使用 wait 和 notify 机制来实现阻塞效果。
对于 入队 来说:就是队列为满。
对于 出队 来说:就是队列为空。
代码如下 :

class MyBlockQueue{
    private int[] data = new int[1000];
    private int size = 0;
    private int head = 0;
    private int tail = 0;
    private Object locker = new Object();
    //入队列
    public void put(int value) throws InterruptedException {
        synchronized (locker){
            if(size == data.length){
                //put 当中的 wait 要由 take 来唤醒,只要 take 成功一个元素,就可以唤醒了
                locker.wait();
            }
            //队列不满,把新的元素放入 tail 位置上
            data[tail] = value;
            tail++;
            //处理 tail 到达数组末尾的情况
            if(tail >= data.length){
                tail = 0;
            }
            size++;
            locker.notify();
        }
    }
    //出队列
    public Integer take() throws InterruptedException {
        synchronized (locker){
            if(size == 0){
                //说明队列为空,就需要等待,就需要 put 来唤醒
                locker.wait();
            }
            int ret = data[head];
            head++;
            if(head >= data.length){
                head = 0;
            }
            size--;
            //就说明 take 成功了。然后唤醒 put 中的等待。
            locker.notify();
            return ret;
        }
    }
}
 public class Example{
    private static MyBlockQueue queue = new MyBlockQueue();
     public static void main(String[] args) {
         //如果有多个生产者和多个消费者,就再多创建几个线程
         Thread producer = new Thread(()->{
             int num = 0 ;
             while(true){
                 try{
                     System.out.println("生产了:"+num);
                     queue.put(num);
                     num++;
                 }catch (InterruptedException e){
                     e.printStackTrace();
                 }
             }
         });
         producer.start();

         Thread customer = new Thread(()->{
            while(true){
                int num = 0;
                try{
                    num = queue.take();
                    System.out.println("消费了:"+num);
                    //消费慢,但是可以一直生产。1000 之后,队列满了,所以就阻塞了。直到消费了一个。
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
            }
         });
         customer.start();
     }
 }

在这里插入图片描述
put和take的相互唤醒之间的关系如下:
在这里插入图片描述


♥♥♥码字不易,大家的支持就是我坚持下去的动力♥♥♥
版权声明:本文为CSDN博主「亚太地区百大最帅面孔第101名」的原创文章

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

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

相关文章

【高效写作技巧】CSDN的原力等级有什么用?如何增长原力等级?

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《写作技巧》 《C嘎嘎干货基地》 ⛺️生活的理想&#xff0c;就是为了理想的生活! &#x1f4cb; 前言 &#x1f308;hello&#xff01; 各位铁铁们大家好啊&#xff0c;这段时间接触了一些刚开始写作的新人…

RPA机器人与传统自动化解决方案的差异性

当前&#xff0c;数字化转型是大势所趋&#xff0c;不少企业在应用传统自动化改造时&#xff0c;由于受到资金、技术、冗杂的流程或者系统集成的限制&#xff0c;投入产出比不甚理想。随着新技术和自动化的升级发展&#xff0c;RPA数字员工为企业数字化转型提供了一套更加简单高…

公众号排版技巧有哪些方面?纯干货

一个精心排版的公众号&#xff0c;能够让读者在阅读过程中感受到舒适与愉悦&#xff0c;同时也能够帮助读者更好地理解文章内容&#xff0c;突出重点&#xff0c;提高阅读效率。 一个专业性的公众号&#xff0c;不仅要有深度的内容&#xff0c;也需要有专业的排版&#xff0c;…

GE IS420UCSBH1A 控制器模块

控制器模块是工业自动化和控制系统中的关键组件&#xff0c;用于监测、控制和管理各种工程过程。这些模块通常具有以下特点&#xff1a; 多通道控制&#xff1a; 控制器模块通常可以控制多个通道&#xff0c;允许同时管理多个设备或过程。 实时控制&#xff1a; 模块支持实时控…

不直播拍视频,一样可以变现,原来是做了这个!

我是电商珠珠 随着直播带货的流行&#xff0c;部分大学也开始紧随其后&#xff0c;相继增设网络营销与直播电商这项课程。 以上这些技能&#xff0c;部分人并不知道怎么搞&#xff0c;一是不想麻烦&#xff0c;没那么多时间精力&#xff0c;二是做不起来&#xff0c;自身没有那…

本地maven批量导入nexus maven仓库中

1、在本地的maven仓库中创建两个脚本 mavenimport.sh里边的内容是&#xff1a; #!/bin/bash # copy and run this script to the root of the repository directory containing files # this script attempts to exclude uploading itself explicitly so the script name is im…

frp内网穿透教程搭建0.52.3版本

网上很多关于frp的教程都是04 03版本的了&#xff0c;都是配置的ini文件&#xff0c;现在都改成toml文件了&#xff0c;下面基本上都是官方文档的简单copy&#xff0c;细节推荐打开去看中文版的文档介绍&#xff08;地址放在最后了&#xff09;。下面简单介绍几个 为什么使用 …

中文互联网正在死去,未来在哪

每次浏览网页的时候&#xff0c;发现优质内容越来越少&#xff0c;大部分的优质内容都是5年之前的。笔者认为2008-2015年是中文互联网最优质的阶段。 尤其是搞IT的&#xff0c;总去网上搜索一些技术难题或者技术文章&#xff0c;现如今发现越来越难&#xff0c;无论是csdn、cnb…

GE IS215VCMIH2CA IS200VCMIH2CAA涡轮燃机模块

涡轮焚机模块是一种用于航空、动力和产业运用的症结装备&#xff0c;具备独特的特征&#xff0c;以餍足高效力、低温、低压的歇息情况。以下是涡轮焚机模块的一些主要特征&#xff1a; 高效力&#xff1a; 涡轮焚机模块通常具备高效的能量变换才能&#xff0c;将焚料焚烧打消的…

上网行为管理软件有哪些丨功能图文超详细介绍

很多人都在后台问&#xff0c;上网行为管理软件到底是什么&#xff0c;有什么作用&#xff0c;今天就重点给大家讲解一下&#xff1a; 是什么 上网行为管理软件可以帮助企业规范员工的上网行为&#xff0c;提高办公效率&#xff0c;减少潜在威胁。 有哪些 在市面上&#xff…

双十一数码好物推荐,这几件入手绝对不吃灰

又到了一年一度的“双十一”购物狂欢季&#xff0c;大家是否已经找到心仪的物品呢&#xff1f;平时我们看中的某个商品&#xff0c;总是希望在最低价时购买&#xff0c;而毫无疑问&#xff0c;双十一是最佳的时机&#xff0c;毕竟各大电商平台都会推出丰富的优惠活动。为此&…

什么是接口测试?如何进行接口测试?

接口测试是一种常见的软件测试方法&#xff0c;用于测试软件系统中不同模块之间的接口。 接口是指两个或多个独立软件模块之间进行数据交换的地方。在软件系统中&#xff0c;不同的模块之间相互依赖和交互&#xff0c;这些模块通过接口来实现数据的传递和共享。因此&#xff0…

实用篇-Gateway网关

前面学的Nacos是对内负载均衡&#xff0c;现在学的Gateway网关是对外负载均衡和校验&#xff0c;不冲突 一、网关的作用 网关功能: 1、身份认证和权限校验 2、服务路由、负载均衡 3、请求限流 在SpringCloud中有两个组件可以实现网关&#xff0c;分别是gateway、zuul Zu…

SpringBoot相比于Spring的优点(自动配置和依赖管理)

自动配置 例子见真章 我们先看一下我们Spring整合Druid的过程&#xff0c;以及我们使用SpringBoot整合Druid的过程我们就知道我们SpringBoot的好处了。 Spring方式 Spring方式分为两种&#xff0c;第一种就是我们使用xml进行整合&#xff0c;第二种就是使用我们注解进行简化…

ansble

ansble概述 Ansible是一款自动化运维工具&#xff0c;基于Python开发&#xff0c;具有批量系统配置,批量程序部署, 批量运行命令等功能。 Ansible的很多模块在执行时都会先判断目标节点是否要执行任务&#xff0c;所以&#xff0c;可以放心大胆地让Ansible去执行任务&#xf…

汇编的各种指令(数据搬移、移位、位运算、算数、比较、跳转、特殊功能寄存器、单寄存器、多寄存器、栈指针指令)

1.汇编指令的格式 2.数据搬移指令---mov mvn 3.移位操作指令 4.位运算操作指令 5.算数运算操作指令 6.比较指令---cmp 7.跳转指令 例子&#xff1a; 8.特殊功能寄存器指令 内存操作指令 9.单寄存操作指令 10.多寄存操作指令 11.栈指针操作指令 例子&#xff1a; 保存现场&…

线程池是如何实现线程复用的?

线程池里面采用了生产者消费者的模式&#xff0c;来实现线程复用。生产者消费者模型&#xff0c;其实就是通过一个中间容器来解耦生产者和消费者的任务处理过程。 生产者不断生产任务保存到容器&#xff0c;消费者不断从容器中消费任务。在线程池里面&#xff0c;因为需要保证工…

gRPC初体验

一、gRPC简介 1、RPC是远程过程调用的简称&#xff0c;在分布式系统中&#xff0c;客户端可以像调用本地对象一样调用远程机器上服务端对象&#xff0c;用于系统的垂直拆分&#xff0c;常见的JAVA RPC框架有JAVA自带的RMI、基于Http的Hessian、阿里基于TCP的Dubbo、淘宝基于TC…

二叉树题目:路径总和 III

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;路径总和 III 出处&#xff1a;437. 路径总和 III 难度 5 级 题目描述 要求 给你二叉树的根结点 root \textt…

VS2022 打包WPF安装程序最新教程(图文详解)

文章目录 前言一、安装打包Installer插件1、单独安装2、VS中在线安装二、使用步骤1、创建安装项目2、安装项目主界面3、添加项目输出4、添加快捷方式图标5、添加卸载项目a、新建项目b、添加项目输出c、创建快捷方式6、给快捷方式添加图标a、在Resource文件夹中添加图标文件b、选…