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

❤️ 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的相互唤醒之间的关系如下:
在这里插入图片描述

定时器

像一个闹钟,在一定时间之后,被唤醒并执行某个之前设定好的任务。

标准库计时器

通过Timer的schedule任务来设计任务计划,Timer内部有专门的线程,来负责执行注册的任务,所以执行完后,并不会马上退出线程,即使所有计划中的任务都已执行完毕,这个内部线程也不会立即结束。它会继续运行,等待其他可能的任务或直到显式地被取消。

public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello Timer");}}, 3000);//就是在 3 秒之后执行这个任务,System.out.println("main");
}

在这里插入图片描述
线程并没有结束,因为 Timer 内部有专门的线程,来负责执行注册的任务的。

线程池

因为进程比较重,频繁的创建和销毁,开销就会大,解决方法:进程池 or 线程:
线程:虽然比进程轻了,但是如果创建和销毁的频率进一步增加,发现开销还是有的,解决方案:线程池 or 协程。
线程池:把线程提前创建好,放到池子里,需要的话,就从池子里取。不用的话,就放回池子里,下次备用。这样创建销毁线程,速度就快了。

用户态和内核态

操作系统中的用户态和内核态。操作系统软件结构图:
在这里插入图片描述
1.我们写的代码就是在最上面的应用程序这一层来运行的,这里的代码被称为”用户态“运行的代码
2.当应用程序需要执行一些底层操作,例如文件访问,网络通信,线程管理等,就需要调用操作系统中提供的API。这些API的内部实现会在内核态运行,这是操作系统的核心部分
3.创建线程的本身就需要内核的支持,创建线程的本质是在内核中搞个 PCB 加到链表里,调用 Thread.start 归根结底,也是要进入内核态来运行
4.线程池是一种高级的编程工具,通常是在用户态实现的。线程池中的线程被预先创建并保留在池中,而不需要频繁地创建和销毁线程。从线程池中获取线程执行任务时,这个过程是在用户态中完成的,不需要涉及到内核态。这提高了效率,因为避免了频繁的内核态切换。
5.一般来说,执行在用户态的操作比需要进入内核态的操作更高效,因为内核态切换会涉及到更多的开销和复杂性。因此,尽量减少进入内核态的操作对于提高程序性能是有益的。
6.线程池里面的线程,一直保存在里面,不会被内核回收。

标准的线程池库

ThreadPoolExecutor 是标准库的线程池,构造方法有很多参数:
在这里插入图片描述
在这里插入图片描述
最重要的还是这两个参数,就是需要指定多少个线程,可以通过性能测试判断出最合适的核心线程数和最大线程数:
在这里插入图片描述


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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机算法分析与设计(22)---回溯法(最小重量机器设计问题)

文章目录 一、问题描述二、算法思路三、代码编写 一、问题描述 设某一机器由 n n n 个部件组成&#xff0c;每种部件都可以从 m m m 个不同的供应商处购得。设 w i j w_{ij} wij​ 是从供应商 j j j 处购得的部件i的重置&#xff0c; c i j c_{ij} cij​ 是相应的价格。设计…

软件开发全文档归档,开发、管理、实施、运维、服务巡检、信息安全、安全运维

在当今高度信息化的时代&#xff0c;软件开发已成为推动社会进步和发展的重要力量。软件开发过程中&#xff0c;文件支撑作为关键的一环&#xff0c;对于保障项目的顺利进行和产品的质量具有不可替代的作用。本文将探讨软件开发所需的主要文件及其作用。 一、引言 软件开发是…

leetCode 746. 使用最小花费爬楼梯 + 记忆化搜索 + 递推 + 动态规划 + 空间优化

关于此题我的往期文章&#xff1a; leetCode 746. 使用最小花费爬楼梯 动态规划-CSDN博客https://heheda.blog.csdn.net/article/details/133325840 dfs(i-1) 跳到 dfs(i) 需要花费 dfs(i-1) cost[i-1]dfs(i-2) 跳到 dfs(i) 需要花费 dfs(i-2) cost[i-2] &#xff08;1&…

C++——list

目录 list介绍 list的函数接口 构造函数 push_front和pop_front push_back和pop_back insert erase 迭代器 front和back size resize empty clear list::sort unique reverse 迭代器的实现 list介绍 list是一种可以在常数范围内在任意位置进行插入和删除的序列…

Java实现Web的ashx对接ORM

之前的介绍已经实现了ORM的主体和Web的调用结构主题&#xff0c;那么这次把Web和LIS.Core的容器和ORM做对接&#xff0c;通过ashx实现的业务类测试调用ORM查询数据。 首先改造容器让传入根地址 package LIS.Core.Context;import org.w3c.dom.Document; import org.w3c.dom.El…

@所有人,城市燃气信息化与信息安全建设方法

关键词&#xff1a;城市燃气信息化、智慧燃气建设、城市燃气安全、智慧燃气、智慧燃气平台 近几年&#xff0c;燃气作为一种新兴的燃料迅速普及开来&#xff0c;和燃气有关的企业之间的竞争也不可避免。身处在互联网的时代&#xff0c;企业只有顺应时代的潮流&#xff0c;将城…

Docker 学习路线 2:底层技术

了解驱动Docker的核心技术将让您更深入地了解Docker的工作原理&#xff0c;并有助于您更有效地使用该平台。 Linux容器&#xff08;LXC&#xff09; Linux容器&#xff08;LXC&#xff09;是Docker的基础。 LXC是一种轻量级的虚拟化解决方案&#xff0c;允许多个隔离的Linux系…

探索 Java 8 中的 Stream 流:构建流的多种方式

人嘛&#xff0c;要懂得避嫌… 开篇引入 Java 8引入了Stream流作为一项新的特性&#xff0c;它是用来处理集合数据的一种函数式编程方式。Stream流提供了一种更简洁、高效和易于理解的方法来操作集合数据&#xff0c;同时也能够实现并行处理&#xff0c;以提高性能。 以下是St…

Cesium:CGCS2000坐标系的xyz坐标转换成WGS84坐标系的经纬高度,再转换到笛卡尔坐标系的xyz坐标

作者:CSDN @ _乐多_ 本文将介绍使用 Vue 、cesium、proj4 框架,实现将CGCS2000坐标系的xyz坐标转换成WGS84坐标系的经纬高度,再将WGS84坐标系的经纬高度转换到笛卡尔坐标系的xyz坐标的代码。并将输入和输出使用 Vue 前端框架展示了出来。代码即插即用。 网页效果如下图所示…

VueX中的getters配置项

一、配置getters属性 当我们想对VueX中的state中的数据进行处理&#xff0c;我们就可以使用getter配置项。 就像是组件中的数据和计算属性之间的关系。 const getters { 属性名 (state) { return 处理结果; } } 我们能够直接拿到state进行操作&#xff0c;并返回操作结果。 …

Shadingsphere proxy 启动报错 Windows

Exception in thread "main" java.lang.NoClassDefFoundError 本来打算在本地电脑测试一下proxy的功能&#xff0c;使用的二进制安装包&#xff0c;没想到怎么都启动不起来&#xff0c;一直报找不到某个类的错误。我还以为是自身的配置有问题&#xff0c;等我copy了…

梯度消失和梯度爆炸的原因

梯度消失和梯度爆炸 梯度爆炸和梯度消失本质上是因为梯度反向传播中的连乘效应。 梯度下降算法 举一个简单的例子,函数表达式为loss 2w^2 4w,如下图 ​​​​​​​ ​​​​​​​ 为了求得w的最优值,使得loss最小,从上图很容易看出来当w -1时,loss最小…

服务器数据恢复—EMC存储pool上数据卷被误删的数据恢复案例

服务器数据恢复环境&#xff1a; EMC Unity某型号存储&#xff0c;连接了2台硬盘柜。2台硬盘柜上创建2组互相独立的POOL&#xff0c;2组POOL共有21块520字节硬盘。21块硬盘组建了2组RAID6&#xff0c;1号RAID6有11块硬盘. 2号RAID6有10块硬盘。 服务器故障&检测&#xff1…

【MySQL】 索引(上)

文章目录 1. 索引的概念2. MySQL与磁盘 的交互基本单位3. 建立共识4. 现象与结论如何理解mysql中page概念为什么 要采用page的方案 进行交互 而不是用多少加载多少&#xff1f; 5. 页目录为什么要引入 页目录概念单页情况多页情况使用B树 构建索引为什么不用其他数据结构为什么…

Classifier-Free Guidance

1.为什么需要分类引导 顾名思义&#xff0c;在原来扩散模型的基础上加上一个引导&#xff0c;让扩散模型朝着我们想要的方向去生成图像 从上图可以了解到生成下一张图像是有分类器参与的 无分类器就是这种形式要参与下一张图像的生成

SQL server数据库端口访问法

最近数据库连接&#xff0c;也是无意中发现了这个问题&#xff0c;数据库可根据端口来连接 网址:yii666.com< 我用的是sql2014测试的&#xff0c;在安装其他程序是默认安装了sql(sql的tcp/ip端口为xxx)&#xff0c;服务也不相同&#xff0c;但是由于比较不全&#xff0c;我…

ElementUI 自定义 Tree 树形控件背景

在 template 中 <div class"container"><el-tree :data"treeList" :props"defaultProps" accordion node-click"handleNodeClick" /> </div> 在 script 中 treeList: [{ id: "-1", label: "区域选…

oracle (8)Managing Tablespace Data File

目录 一、基础知识 1、表空间和数据文件 2、存储层次结构摘要 3、表空间的类型 4、表空间中的空间管理 5、临时表空间 6、Default Temporary TS 默认临时TS 二、常用实操 1、Creating Tablespaces创建表空间 2、Dictionary-Managed TS 字典管理的表空间 3、Locally …

uniapp 关于 video 组件的缩放比例问题

在 container 样式的 padding-bottom 设置比例值 9/16 比例值&#xff1a;56.25% 3/4 比例值&#xff1a;75% <view class"container"><video class"video-box" src"xxx.mp4" /> </view> .container {position: relative;wid…

与AI对话的艺术:如何优化Prompt以获得更好的响应反馈

前言 在当今数字化时代&#xff0c;人工智能系统已经成为我们生活的一部分。我们可以在智能助手、聊天机器人、搜索引擎等各种场合与AI进行对话。然而&#xff0c;要获得有益的回应&#xff0c;我们需要学会与AI进行有效的沟通&#xff0c;这就涉及到如何编写好的Prompt。 与…