[Java EE] 多线程(五):单例模式与阻塞队列

devtools/2024/11/15 4:08:49/

1. 单例模式

单例模式是校招中最长考的设计模式之一,首先我们来谈一谈什么是设计模式:

设计模式就好像象棋中的棋谱一样,如果红方走了什么样的局势,黑方就有一定地固定地套路,来应对这样的局势,按照固定地套路来,可以保证在该局势下不会吃亏.
在这里插入图片描述
软件开发也是同样的道理,有很多的问题场景,针对这些问题场景,一些编程界的大佬就总结出了一套固定的解决模版,只要按着这个套路来,就会避免出现各种各样的bug.

软件设计中的单例模式就是保证每个程序运行的过程中只存在一份实例,而不会创建多个实例.这种设计模式在我们的日常开发中比比皆是,使用单例模式也可以大大提高程序的效率,下面我们来举个例子:
假如服务器中药从硬盘上读取100G的固定数据到内存中,这样的操作肯定是通过一个类来封装,这样的类最好是单例的,因为数据一样,在创建多个实例的时候,就会对系统资源开销很大,而且多次读取同样数据这样的操作也没必要,让他们共享同一份数据就可以了.

但是如何保证每个类只创建一个实例出来呢?这可不是程序员口头保证就行,只要是人去做,都不靠谱,我们应该把这样的操作交给计算机来处理,需要让编译器来帮我们做一个强制的检查.

通常的单例模式有以下两种,懒汉模式和饿汉模式:

1.1 饿汉模式

所谓饿汉模式,就突出一个字:,就好像一个饿汉被饿了好几天想要得到食物一样.我们需要在JVM启动的时候,即类刚刚加载的时候,就创建这个类的实例.其次,我们为了该类只创建一个实例,防止通过构造方法来创建多余的实例,所以我们把构造方法设置为私有.之后要想获取这个类的实例,我们可以通过一个普通方法来获取类中创建的这个实例.代码如下:

java">/*** 饿汉模式*/
class Singleton{public static Singleton instance = new Singleton();//在加载类的时候就创建实例private Singleton(){}//通过把构造方法设计为private修饰的来避免调用构造方法public static Singleton getInstance(){return instance;}
}

在多线程情况下,饿汉模式仍然是线程安全的:因为在创建这个类的时候,这个实例就已经有了,它的创建比主线程的启动都要早,所以在主线程中启动其他线程的时候,只需要读取已经创建好的线程就可以,我们前面说到,针对同一个变量进行修改的时候容易产生线程安全问题,但是现在只有读取操作,所以不存在线程安全问题.

1.2 懒汉模式

所谓懒汉模式,就突出一个字:,在第一次调用这个类的实例的时候才创建实例.如果没有被创建,引用一直为null.
单线程情况:

java">/*** 懒汉模式*/
class SingletonLazy{Object object = new Object();public static SingletonLazy instance = null;private SingletonLazy(){}public static SingletonLazy getInstance(){if (instance == null){//判断实例是否被创建过,如果被创建过,就不创建了,直接返回instance = new SingletonLazy();}return instance;}
}

多线程情况:
上述代码虽然说在单线程情况下不会出现什么bug,但是在多线程的情况下,就会难免出现bug,下面我们就通过画图的方式解释以下为什么会出现bug.
在这里插入图片描述
在线程1和线程2创建实例的时候,线程2的条件判断有可能在线程1创建实例执之前,此时线程1和线程2都会创建实例,从而违背了单例模式的初衷.这里产生线程安全问题的原因是操作不符合原子性.
那么为了避免在多线程中的安全问题,我们可以通过加锁的方式,把创建对象这个操作打包成一个原子的操作.

java">/*** 懒汉模式*/
class SingletonLazy{Object object = new Object();public static SingletonLazy instance = null;private SingletonLazy(){}public static SingletonLazy getInstance(){synchronized (instance.object){if (instance == null){//防止多个线程同时创建多个对象instance = new SingletonLazy();}}return instance;}
}

但是上述代码在一些方面还是显得有些赘余:

  1. 如果线程比较多,会加重锁竞争,在最初调用getInstance的时候会存在线程安全问题,但是后续调用,就只是读取操作,并不存在线程安全问题,所以此时上锁就是赘余,上锁本身也是一个开销比较大的操作.
  2. 存在内存可见性问题和指令重排序问题导致读取instance的偏差.
    为了解决上述问题,我们对上述代码进行优化:
java">/*** 懒汉模式*/
class SingletonLazy{Object object = new Object();public static volatile SingletonLazy instance = null;//加上volatile方式指令重排序和内存可见性引起的bugprivate SingletonLazy(){}public static SingletonLazy getInstance(){if (instance == null){//节省上锁的开支synchronized (instance.object){if (instance == null){//防止多个线程同时创建多个对象instance = new SingletonLazy();}}}return instance;}
}

这里两层判断的条件虽然是一样的,但是它们所起到的作用,却是大相径庭:
第一层判断是为了创建好实例之后,在后续获取实例的时候重复加锁,已节省系统开销.
第二层判断是为了多个线程如果都在创建好实例之前运行,防止这几个线程同时创建多个实例.

举例说明:
有请助教:荧,温迪,莱欧斯利,原神其他男性角色
现在温迪和莱欧斯利(两个线程)都知道了荧没有男朋友(没有创建实例),他们都突破了第一层if,于是开始竞争统一把锁.
在这里插入图片描述
假如现在风神巴巴托斯(线程1)更胜一筹:温迪就通过了里层的if,成为了荧的男朋友(创建了实例):
在这里插入图片描述
现在莱欧斯利(线程2)拿到了锁,但是被里层if排除在了外面,因为荧已经有了男朋友(实例已经创建了).
在这里插入图片描述
其他人听说了荧已经有了男朋友(已经创建了实例),纷纷散去了(被外层if排除在了外面).
在这里插入图片描述
在这里插入图片描述

2. 阻塞队列

2.1 什么是阻塞队列

阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则.
阻塞队列能是⼀种线程安全的数据结构(但是像我们前面学习的普通队列和优先级队列都是线程不安全的),并且具有以下特性:
• 当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素.
• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素.
阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型.

拓展:消息队列
还有一种队列,叫做消息队列,首先通过topic这样的参数,对数据进行归类,出队列的时候,指定topic,每个topic下的所有数据先进先出,消息队列往往也带有阻塞特性.由于这种队列实在是太好用了,在部署服务器的时候,一般对这种队列会进行单独部署.

2.2 标准库中的阻塞队列

在Java标准库中内置了阻塞队列.如果我们需要在⼀些程序中使⽤阻塞队列.直接使⽤标准库中的即
可.
• BlockingQueue是⼀个接⼝.真正实现的类是LinkedBlockingQueue / ArrayBlockingQueue / PriorityBlockingQueue.
put方法用于阻塞式的入队列,take用于阻塞式的出队列.
• BlockingQueue也有offer,poll,peek等⽅法,但是这些⽅法不带有阻塞特性.

2.3 生产者消费者模型

⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题
⽣产者和消费者彼此之间不直接通讯, 而通过阻塞队列来进行通讯所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取.
在这里插入图片描述
生产者和消费者模型有以下好处:

  1. 阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒.(削峰填谷)
    场景: 天猫11·11秒杀商品,在双十一的零点,阿里巴巴的服务器会接收到大量的支付请求,但是这样请求又是复杂的,很有可能服务器就被这样的一波请求冲垮了,这时候便会用到阻塞队列,让服务器慢慢处理这些指令.
    这样做可以有效进⾏"削峰",防⽌服务器被突然到来的⼀波请求直接冲垮.
    在这里插入图片描述

  2. 阻塞队列也能使⽣产者和消费者之间解耦.
    如果直接连接,服务器A和服务器B之间的逻辑关联会非常强,如果A或者B出现了bug或者出现了修改,另一个服务器就会被牵连到.这样的耦合性就比较高.我们希望通过阻塞队列来降低耦合性.让两个服务器和阻塞队列关联.

举例:过年包饺子
有请助教:空,荧
包饺子需要两步,擀皮,包馅,假如荧擀皮(生产者),空包馅(消费者),荧擀完皮会放在他们中间的盖帘上(阻塞队列).
在这里插入图片描述
假如荧擀皮慢,空包馅快,空在盖帘空的时候,空就会阻塞等待.
假如空包馅慢,荧擀皮快,空在盖帘满的时候,荧就会阻塞等待.

下面我们来用代码实现一个生产者消费者模型:

java">import java.util.concurrent.LinkedBlockingQueue;/*** 生产者与消费者模型*/
public class Demo23 {public static void main(String[] args) {BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();Thread thread = new Thread(()->{int count = 1;while (true){try {blockingQueue.put(count);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("put:" + count);count++;try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread thread1 = new Thread(()->{int num = 0;while (true){try {num = blockingQueue.take();//当队列为空的时候就阻塞等待} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("take:" + num);}});thread.start();thread1.start();}
}

2.4 阻塞队列的实现

  • 通过"循环队列"的方式实现.
    循环队列描述
  • 使用synchronized进行加锁控制.
  • put插⼊元素的时候,判定如果队列满了,就进⾏wait.(注意,要在循环中进⾏wait.被唤醒时不⼀定
    队列就不满了,因为还有可能是其他的线程的interrupt唤醒了该线程
    ).
  • take取出元素的时候,判定如果队列为空,就进⾏wait.(也是循环wait)
java">/*** 实现阻塞队列*/
public class Block {public int size = 0;public int tail = 0;public int head = 0;public int[] item = new int[10];//默认循环队列的容积是10public int take() throws InterruptedException {//取队列元素synchronized (this){int ret = 0;while (size == 0){this.wait();}ret = item[head];//处队头元素head = (head+1)%item.length;size--;this.notify();//当出了一个元素之后,唤醒put的wait()return ret;}}public void put(int num) throws InterruptedException {synchronized (this){while (size == item.length){//队列满,阻塞等待this.wait();}item[tail] = num;tail = (tail+1)%item.length;size++;this.notify();//添加一个元素之后,唤醒take的阻塞}}
/**
*开始测试
**/public static void main(String[] args) {Block block = new Block();Thread thread = new Thread(()->{int num = 1;while (true){try {block.put(num);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("put:"+num);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}num++;}});Thread thread1 = new Thread(()->{int ret = 0;while (true){try {ret = block.take();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("take:"+ret);}});thread.start();thread1.start();}
}

测试结果:
在这里插入图片描述


http://www.ppmy.cn/devtools/24183.html

相关文章

运营如何实现月入过万?这3个思路一定要有!

要想在竞争激烈的流量市场中脱颖而出并取得成功&#xff0c;并非易事。 今天将分享3个运营的关键思路&#xff0c;帮助大家实现月入过万的运营目标。 第一个&#xff1a;搭建素材库 搭建专属素材库是提升运营效率的重要一步。运营者需要收集、整理和管理与目标受众相关的素材…

Cesium学习——渲染、加载GeoJSON、调整位置

渲染概述 作者&#xff1a;当时明月在曾照彩云归 出处&#xff1a;https://www.cnblogs.com/jiujiubashiyi/p/17124717.html 1. 引言 Cesium是一款三维地球和地图可视化开源JavaScript库&#xff0c;使用WebGL来进行硬件加速图形&#xff0c;使用时不需要任何插件支持&#xf…

各类数据引擎指定schema或者数据库

背景 以hive为例&#xff0c;如果某个hive引擎&#xff0c;有my_test、test_dd、test_xx、aqtest0228数据库某张表的数据&#xff0c;如果需要查询某个库的数据&#xff0c;需要带库名&#xff0c;否则会报错。 sql语句如下 SELECT * FROM aqtest0228.t_bz_zl_ss_zyxx_wrw…

客户端连接ZK失败处理方案

文章目录 背景介绍报错信息处理方案第一步、查看zookeeper启动是否正常第二步、检查本地网络是否正常第三步、检查本地JDK版本 对于zookeeper服务注册中心&#xff0c;在前期【 Dubbo框架注册中心-Zookeeper搭建】博客中有环境搭建部署介绍&#xff0c;感兴趣可以参考安装。 背…

【Unity动画系统】详解Root Motion动画在Unity中的应用(二)

Root Motion遇到Blend Tree 如果Root Motion动画片段的速度是1.8&#xff0c;那么阈值就要设置为1.8&#xff0c;那么在代码中的参数就可以直接反映出Root Motion的最终移动速度。 Compute Thresholds&#xff1a;根据Root Motion中某些数值自动计算这里的阈值。 Velocity X/…

mysql面试题九(SQL优化)

目录 1.一条 SQL 是如何执行的 2.索引失效的几种情况 3.EXPLAIN 4.Where 子句如何优化 5.超大分页或深度分页如何处理 6.大表查询如何优化 7.分库分表 基本概念 分库分表方法 水平拆分 垂直拆分 分库分表后的注意事项 1.一条 SQL 是如何执行的 在MySQL中&#xff0…

【Spring Data JPA】实体对象生命周期相关注解

文章目录 PrePersist 注解PreUpdate 注解PreRemove 注解PostLoad 注解PostPersist 注解PostRemove 注解使用案例启动类Entity 对象生命周期&#xff08;Listener&#xff09; PrePersist 注解 PrePersist 注解用于指定在将实体对象持久化到数据库之前执行的方法。通常用于设置…

Stable Diffusion 模型分享:Inkpunk Diffusion(动漫、墨水朋克)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 在 Dreambooth 上训练的微调稳定扩散模型。隐约受…