剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池
作者:困了电视剧
专栏:《JavaEE初阶》
文章分布:这是关于java多线程案例的文章,进行了对单例模式、阻塞队列及生产者消费者模型、定时器和线程池的讲解,希望对你有所帮助!
目录
单例模式
懒汉模式实现
饿汉模式实现
阻塞式队列
标准库中的阻塞式队列
生产者消费者模型
降低耦合
削峰填谷
低配版阻塞式队列的简单自我实现
定时器
标准库中的定时器
低配版定时器的简单自我实现
线程池
标准库中的线程池
代码实现
创建线程池的参数的意义
线程池的拒绝策略
低配版线程池的简单自我实现
单例模式
单例模式是一个非常常见的设计模式。
什么是设计模式?
设计模式好比象棋中的 "棋谱". 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏. 软件开发中也有很多常见的 "问题场景". 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照 这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。我们在日常的编程中一定有类似这样的需求,比如我们现在有一个wife类,这个wife类只能实例化一个“wife”,否则就会发生逻辑上的错误,这时我们就需要这个单例模式来约束我们只能创建一个wife对象。单例模式主要分为“饿汉”和“懒汉”两种具体实现方式。
懒汉模式实现
以洗碗举例,懒汉模式就是我中午吃完饭,我留着碗不急着洗,等我到晚上需要再次吃饭的时候,我再去洗,而且我只洗我吃晚饭需要的碗,不需要的碗我仍然不洗,直到我需要为止。
抛开现实的卫生因素,这样做的效率会比我吃完就全洗的效率更高,占用的资源更少,具体到代码中就是,只有当我需要他的实例时我在创建,不需要就不创建。
class Singleton{//饿汉模式不调用就不创建实例//用static进行修饰,使其成为一个类变量,这样这个类中就只有一个private static Singleton instance = null;public static Singleton getSingleton(){//当这个不为空的时候,此时就是单纯的读操作,不需要再像第一次执行一样考虑线程安全问题if (instance == null){synchronized (Singleton.class){//判断是否为空,为空就进行实例化if (instance == null){instance = new Singleton();}}}return instance;}//解决了用方法进行调用的且只有一个需求,现在还需要封印用new来进行实例化的步骤,用private修饰构造方法即可private Singleton(){}
}
加锁是因为在多线程环境下,可能会导致不同线程new出来的对象不同所以要加锁。
同时这里的两个判断instance为空的条件虽然写了两次,但不可省略,原因如下:
1.最重要的一点是,初心不同,第一个判断是为了判断是否需要在进行写操作,如果只需要 进行读操作的话则不需要加锁,以提高线程的执行效率。加锁是一个非常低效的操作,所 以,非必要不加锁。第二个判断则是判断是否为空,为空则需要进行实例化,这里两者一 样,只是一种巧合。
2.虽然这两个判断条件只隔有一个锁,但是在锁竞争的影响下他们之间被执行的间隔可能会 是很长时间,在这段时间里无法保证instance没有被删掉。
饿汉模式实现
饿汉模式就是,我很“饿”,所以我很“勤快”,当我中午饭吃完后我立即就把碗什么的全给洗了,在代码中的实现也是,我一开始就将其进行赋值,用不用,什么时候用不关心。
class Singleton{private static Singleton instance = new Singleton();public static Singleton getSingleton(){return instance;}private Singleton(){}
}
阻塞式队列
阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则。
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
1.当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
2.当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型。
标准库中的阻塞式队列
put方法是向队列中加入元素,而take方法则是从队列中拿出元素。
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等 待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
降低耦合
举个栗子:比如现在有A,B两个服务器,A是生产者,B是消费者,现在我们不采用这种生产者消费者模型,那A和B就相当于直接进行交互,假如A向B发送一个请求,B给A一个回应,如果此时A崩了,那B也就没什么用了,即也就跟着崩了,这就是强耦合。
而如果此时我们在中间加一个阻塞队列服务器,即采用生产者消费者模型。
这样A和B两个服务器只认识这个阻塞队列服务器,并不认识对方,此时,如果A崩了,那B并不会受到太大的影响。这就大大降低了两个服务器之间的耦合程度,提高了稳定性。
削峰填谷
这个模型还有一个巨大的作用就是削峰填谷。
现在依旧是A,B两个服务器,假设A服务器是一个与网购相关的服务器,在某一时刻有一个促销活动,这个活动的出现使A服务器发送的请求在短时间内大大增加,这是就对需要对其进行回应的B服务器造成了极大的负担,很可能会导致服务器的的崩溃!
这时如果我们使用生产着消费者模型,那爆炸式增长的请求就会直接进入到阻塞队列服务器,而B服务器就会按照“自己”的速度一一进行回应,“峰”是这样,“谷”也是如此。
注:由于阻塞队列服务器的业务较少,所以相比于业务很多的A和B服务器,要稳定很多。
低配版阻塞式队列的简单自我实现
public class MyBlockingQueue {//循环队列但是加上了等待阻塞private int[] items = new int[1000];volatile private int size = 0;volatile private int head = 0;volatile private int tail = 0;synchronized public void put(int elem) throws InterruptedException {if (size == items.length){this.wait();}items[tail] = elem;tail++;size++;if (tail == items.length){tail = 0;}//此时阻塞队列已经put出了一个元素,也就是说现在一定不满,此时进行阻塞等待的就可以被唤醒了this.notify();}synchronized public int take() throws InterruptedException {if (size == 0){this.wait();}int ret = items[head];head++;size--;if (head == items.length){head = 0;}this.notify();return ret;}
}
简易版的实现,希望对你有所启发。
定时器
标准库中提供了一个 Timer 类.。
Timer 类的核心方法为 schedule . schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).
标准库中的定时器
TimerTask是一个实现了Runnable接口的抽象类,但由于不是函数式接口所以不能使用Lambda表达式进行书写。
三秒后在控制台中打出了Hello。
低配版定时器的简单自我实现
class MyTask implements Comparable<MyTask> {public Runnable runnable;public long time;public MyTask(Runnable runnable,long delay){this.runnable = runnable;//这里的time是时间戳,所以需要先找到目前的时间戳然后加上需要延时的时间this.time = System.currentTimeMillis() + delay;}//因为要放入带有优先级的阻塞队列中所以我们要指定一个比较规则,实现comparable接口@Overridepublic int compareTo(MyTask o) {return (int)(this.time - o.time);}
}public class MyTimer {//这个结构,带有优先级的阻塞队列,核心数据结构private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();private Object locker = new Object();//这个“时间表”方法,就是将我们的任务放到队列中public void shedule(Runnable runnable,long delay){MyTask myTask = new MyTask(runnable,delay);queue.put(myTask);locker.notify();}//这个构造方法是执行我放到“时间表”中的任务,当我实例化开始就开始计时public MyTimer(){Thread t = new Thread(() -> {while (true){try {synchronized (locker){MyTask myTask = queue.take();long curTime = System.currentTimeMillis();if (myTask.time <= curTime){myTask.runnable.run();}else{queue.put(myTask);locker.wait(myTask.time - curTime);}}} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}
}
相关代码已用注释标记出了意思,如有不清楚的地方还请评论区讨论~~
线程池
在CPU内部,每次需要使用一个线程都需要重新创建并且销毁,这每次的创建销毁都需要消耗资源,那能不能这样做,我一次创建一定数量的线程,并把它们放到一个名字叫“线程池”的容器中,这样,每当我们在需要用线程的时候就不需要再重新创建了,并且当这个线程的run方法执行完毕后就不需要在销毁这个线程,而是直接将这个线程再放进线程池当中以便下次使用,这样就又节省了销毁线程的资源。
举个栗子:
我现在开了一个小卖部,我每次需要去送外卖,不用线程池就是每次需要送外卖的时候都重新雇一个人,然后他送完外卖后我立刻将他解雇。
而线程池就是,你一次雇几个人,然后这几个人去送外卖,他们送完后也不解雇等着下次外卖到来。这样就节约了很多的资源。
标准库中的线程池
代码实现
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadDemo3 {public static void main(String[] args) {//通过Executors中的静态方法来进行线程池的创建,现在这个线程池中有十个线程ExecutorService pool = Executors.newFixedThreadPool(10);//我们可以通过submit方法来注册一个任务到线程池当中for ( int i=0;i<1000;i++ ){int num = i;pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello" + num);}});}}
}
这里我们不能通过直接new来创建对象,必须要通过调用静态方法才行,这时一个叫做工厂模式的设计模式,在我们日常的一些需求中,仅仅使用构造方法的重载无法满足,有时候就需要工厂模式来帮助我们,举个栗子:
创建一个point对象,我需要描述他的坐标,假设是二维平面,我们可以通过x和y来进行描述,这时我们构造方法传两个参数就行了,但是用极坐标的ρ和角度也可以进行创建,这时候我们就发现这两种构造方法传的参数类型和数量都是一样的,无法区分开,于是工厂模式应运而生。
Executors 创建线程池的几种方式
newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装.
这段代码的执行结果为:
十个线程会将这些任务执行完,随机没有顺序。
创建线程池的参数的意义
corePoolSize是核心线程数
maxmumPoolSize是最大线程数
当目前任务数量较多时,线程池会多创建一些临时线程。
核心线程就相当于“正式员工”,而这些临时线程就相当于“实习生”,当任务变少时,线程池就会考虑将这些“实习生”辞退,即销毁这些临时线程,但核心线程则不会受影响会一直保留。
long keepAliveTime是临时线程保持存活的时间
TimeUnit unit是时间的单位
当任务较少时,这些临时线程并不会立即被销毁,而是暂时存活一段时间,因为线程池不确定过一会会不会又有大量的任务加进来,所以会保留一段时间来节约资源。
workQueue是阻塞队列
threadFactory是工厂模式
线程池要管理很多个任务,这些任务的底层就是通过阻塞队列来管理的,程序员可以手动指定一个阻塞队列,这样就可以清楚地知道队列中的信息,submit方法就是将任务放到这个阻塞队列中。
工厂模式就是创建辅助线程的类
handler是线程池的拒绝策略。
线程池的拒绝策略
我们可以将任务添加到线程池当中来提高我们程序运行的效率,但是线程池的承载能力毕竟是有限的,当我们向其中添加的任务过多时,线程池就会拒绝任务的添加,标准库提供了四种拒绝策略。
1.如果满了还继续加任务,那添加操作就会直接抛出异常,新任务老任务都无法执行。
2.添加的线程自己负责执行这个任务,即劳资不仅不干,劳资还要怼回去。
3.丢弃最老的任务,将新的任务添加。
4.丢去最新的任务,即不理这个新任务,该咋咋还咋咋。
低配版线程池的简单自我实现
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;public class MyThreadPool {//用阻塞队列来存放任务private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();public void submit(Runnable runnable) throws InterruptedException {queue.put(runnable);}public MyThreadPool(int n){//创建n个线程for ( int i=0;i<n;i++ ){Thread t = new Thread(() ->{//每个线程内部做的任务是,只要队列中有任务我就抢着执行try {while (true){Runnable runnable = queue.take();runnable.run();}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}}
}
相关代码注释已给出。
以上就是本篇博客的全部内容,如有疏漏还请指正!
相关文章:

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池
作者:困了电视剧 专栏:《JavaEE初阶》 文章分布:这是关于java多线程案例的文章,进行了对单例模式、阻塞队列及生产者消费者模型、定时器和线程池的讲解,希望对你有所帮助! 目录 单例模式 懒汉模式实现 饿…...

Guitar Pro8中文版更新说明及系统要求介绍
Guitar Pro吉他软件是初学作曲,特别是同时又初学吉他的朋友们的良师益友,是一款极佳的初级软件,是非实时作曲软件之中的一件佳作。Guitar Pro在吉他和弦、把位的显示、推算、查询、调用等方面,也异常方便、简洁、直观和浩瀚&#…...

【id:19】【20分】A. 三数论大小(引用)
题目描述 输入三个整数,然后按照从大到小的顺序输出数值。 要求:定义一个函数,无返回值,函数参数是三个整数参数的引用,例如int &a, int &b, int &c。在函数内对三个参数进行排序。主函数调用这个函数进行…...
To_Heart—总结——FWT(快速沃尔什变换)
目录闲话拿来求什么或与异或闲话 这个比FFT简单了很多呢,,大概是我可以学懂的水平! 好像是叫 快速沃尔什变换 ? 拿来求什么 以 FFT 来类比。我们 FFT 可以在 O(nlogn)\mathrm{O(nlogn)}O(nlogn) 的复杂度下实现求解࿱…...

Google巨大漏洞让Win10、11翻车,小姐姐马赛克白打了
早年间电脑截图这项技能未被大多数人掌握时,许多人应该都使用过手机拍屏幕这个原始的方式。 但由于较低的画面质量极其影响其他用户的观感,常常受到大家的调侃。 但到了 Win10、11 ,预装的截图工具让门槛大幅降低。 WinShiftS 就能快速打开…...

腾讯云服务器部署内网穿透(让其他人在不同ip可以访问我们localhost端口的主机项目)(nps开源项目)
首先打开shell连接我们的云服务器 然后我们再opt目录下面创建一个文件夹用来存放我们的压缩包和文件 mkdir /opt/nps 这个是它官方的安装图解.所以我们按照这个docker安装过程来: 然后我们用docker安装镜像.这样的话比较简单一点 docker pull ffdfgdfg/nps 然后我们查看docker…...

IDS、恶意软件、免杀技术、反病毒技术、APT、对称加密、非对称加密以及SSL的工作过程的技术介绍
IDS的简单介绍IDS是:入侵检测系统(intrusion detection system,简称“IDS”)是一种对网络传输进行即时监视,在发现可疑传输时发出警报或者采取主动反应措施的网络安全设备。它与其他网络安全设备的不同之处便在于&…...

怎么把pdf转换成高清图片
怎么把pdf转换成高清图片?可以使用以下两种方法: 方法一:使用Adobe Acrobat Pro DC 1、打开需要转换的PDF文件,点击“文件”菜单中的“导出为”,在弹出的菜单中选择“图像”,然后选择“JPEG”。 2、在“…...

MATLAB 系统辨识 + PID 自动调参
系统辨识 PID 自动调参 文章目录系统辨识 PID 自动调参1. 导入数据1.1 从 Excel 中导入数据2. 系统辨识3. PID 自动调参1. 导入数据 1.1 从 Excel 中导入数据 如果不是从Excel中导入可以跳过该步骤 导入函数: [num,txt,raw]xlsread(xxx\xxx.xlsx);num返回的是…...

【vue3】组合式API之setup()介绍与reactive()函数的使用·上
>😉博主:初映CY的前说(前端领域) ,📒本文核心:setup()概念、 reactive()的使用 【前言】vue3作为vue2的升级版,有着很多的新特性,其中就包括了组合式API,也就是是 Composition API。学习组合…...
爬虫Day3 csv和bs4
爬虫Day3 csv和bs4 一、CSV的读和写 1. 什么是csv文件 csv文件叫做:逗号分隔值文件,像Excel文件一样以行列的形式保存数据,保存数据的时候同一行的多列数据用逗号隔开。 2. csv文件的读写操作 1) csv文件读操作 from csv import reader…...

nnAudio的简单介绍
官方实现 https://github.com/KinWaiCheuk/nnAudio; 论文实现: nnAudio: An on-the-Fly GPU Audio to Spectrogram Conversion Toolbox Using 1D Convolutional Neural Networks; 以下先对文章解读: abstract 在本文中&#x…...
【id:134】【20分】B. 求最大值最小值(引用)
题目描述 编写函数void find(int *num,int n,int &minIndex,int &maxIndex),求数组num(元素为num[0],num[1],...,num[n-1])中取最小值、最大值的元素下标minIndex,maxIndex(若有相同最值࿰…...
Java 面向对象
一、Java 8 增强的包装类 Java是面向对象的编程语言,但它也包含了8种基本数据类型,这8种基本数据类型不支持面向对象的编程机制,基本数据类型的数据也不具备对象的特性。(没有成员变量、方法可以被调用)。Java之所以提…...

五、传输层
(一)TCP传输控制协议 可靠的、面向连接的字节流服务,全双工,有端口寻址功能 1、TCP的三种机制 1.使用序号对分段的数据进行标记,便于调整数据包 2.TCP使用确认、校验和和定时器系统提供可靠性 3.TCP使用可变大小的…...
Thinkphp 6.0一对一关联查询
本节课我们来了解关联模型中,一对一关联查询的使用方法。 一.hasOne 模式 1. hasOne 模式,适合主表关联附表,具体设置方式如下: hasOne(关联模型,[外键,主键]); return $this->hasOne(Profile::class,user_id, id); 关联模型&…...

基于51单片机的自动打铃打鸣作息报时系统AT89C51数码管三极管时钟电路
wx供重浩:创享日记 对话框发送:单片机打铃 获取完整无水印论文报告说明(含源码程序、电路原理图和仿真图) 本次设计中的LED数码管电子时钟电路采用24小时制记时方式,本次设计采用AT89C51单片机的扩展芯片和6个PNP三极管做驱动&…...
算法详解-双指针算法的魅力-一种简单而高效的编程思想
文章目录双指针简介快慢指针快慢指针介绍快慢指针例题快慢指针优缺点:对撞指针对撞指针介绍:对撞指针例题对撞指针优缺点:更新中——未完总结更多宝藏双指针简介 😎🥳😎🤠😮&#x…...

网页审查元素
在讲解爬虫内容之前,我们需要先学习一项写爬虫的必备技能:审查元素(如果已掌握,可跳过此部分内容)。1、审查元素在浏览器的地址栏输入URL地址,在网页处右键单击,找到检查。(不同浏览器的叫法不同…...
gpt2 adapter finetune
1. 安装依赖: pip install -U adapter-transformers pip install datasets 2.训练代码: from datasets import load_dataset from transformers import AutoModelForCausalLM from transformers import GPT2Tokenizer from transformers import Adap…...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...

3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...
【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error
在前端开发中,JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作(如 Promise、async/await 等),开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝(r…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...