Java多线程实战-从零手搓一个简易线程池(一)定义任务等待队列

news2024/9/23 22:37:23

🏷️个人主页:牵着猫散步的鼠鼠 

🏷️系列专栏:Java全栈-专栏

🏷️本系列源码仓库:多线程并发编程学习的多个代码片段(github)

🏷️个人学习笔记,若有缺误,欢迎评论区指正 

目录

前言

等待队列是什么

为什么需要等待队列

实现思路

代码实现

1.新建BlockQueue类

2.任务的添加和获取方法

定义阻塞添加任务方法put

编写堵塞拿取任务方法take

带超时时间的阻塞添加方法offer

带超时时间的阻塞获取方法poll

总结


✨️本系列源码均已上传仓库 1321928757/Concurrent-MulThread-Demo(github.com)✨️

前言

在多线程编程中,线程池是一种非常重要的工具。它可以帮助我们高效地管理线程资源,避免频繁创建和销毁线程带来的性能开销。Java中提供了强大的线程池实现,如 ThreadPoolExecutor ,但有时我们可能希望了解其内部原理,并实现一个简单的线程池来加深对其工作机制的理解,手写线程池也是很多大厂常考的笔试题。

开个新坑-手搓简易线程池。本系列文章将从零开始,一步步手工编码实现一个简单但功能完备的线程池。我们将逐步介绍线程池的各个核心组件,并分析它们的作用和实现思路。通过这个过程,我们可以更好地掌握多线程编程的技巧,并培养编码能力和系统性思维。

本篇文章的任务是带领大家定义任务等待队列,任务等待队列是线程池中一个十分重要的组成部分,在各种生产者-消费者模型场景下十分常见,如下我们先来介绍一下等待队列。

等待队列是什么

等待队列(Task Queue)是一种用于临时存储任务的队列数据结构。在多线程环境下,它常被用作生产者-消费者模型中的"缓冲区",用于平衡任务的产生速度和任务的处理速度之间的差异。

等待队列本质上是一个先进先出(FIFO)的队列,新加入的任务会被存储在队列尾部,而消费者则从队列头部获取任务执行。根据队列的实现方式不同,它可以是无界的或者有界的。无界队列理论上可以存储无限多的任务,而有界队列则有最大容量限制。

我们这里的等待队列底层可以借助JDK提供的双端队列ArrayDeque实现。

为什么需要等待队列

在现实场景中,任务的产生速度往往是不均匀的,而执行任务的线程资源又是有限的。如果没有等待队列,当任务瞬间扩增时,可能会出现以下问题:

  1. 任务无法被及时消费,导致任务丢失或被阻塞。
  2. 需要临时创建大量线程来处理任务,线程的创建和销毁开销巨大,影响系统性能。
  3. 任务的执行顺序无法得到保证,可能导致某些重要任务长期得不到执行。

引入等待队列后,生产者可以先将任务存入队列,而消费者则持续从队列中获取并执行任务。这种"缓冲"机制可以有效应对任务瞬间扩增的情况,并保证任务按先后顺序被逐个消费。

此外,通过设置有界队列,我们还可以限制队列的最大容量,防止任务无限堆积导致内存溢出。当队列已满时,我们可以采取拒绝策略(如直接丢弃、暂存等)来应对新加入的任务。

如下图,任务等待队列其实就是联系任务生成者与任务消费者的一个桥梁,生产者生产消息放入等待队列中,再由消费者拿取消费。

实现思路

为了实现一个功能完备的任务等待队列,我们需要设计一个阻塞队列BlockQueue,它具有以下几个关键特性:

  1. 有界队列 BlockQueue将设置一个固定的容量size,队列中最多只能存储size个任务。这样可以防止任务无限制地堆积,导致内存溢出。当队列满时,新添加的任务将被阻塞,直到队列中有空位。

  2. 线程安全 BlockQueue的操作,包括添加任务put和获取任务take,都需要保证线程安全。我们将使用Java的重入锁ReentrantLock和条件变量Condition来实现线程的正确同步。

  3. 支持超时 在添加或获取任务时,BlockQueue将提供带超时时间的方法offer和poll。这样可以防止任务被无限期阻塞,提高系统的响应性和健壮性。

基于以上思路,BlockQueue的实现将涉及以下几个核心部分:

  1. 使用双端队列Deque作为底层数据结构存储任务
  2. 使用ReentrantLock和两个Condition(emptyCondition和fullCondition)来实现阻塞和唤醒机制
  3. 实现put、offer、take、poll等核心方法,正确控制任务的添加和获取

代码实现

1.新建BlockQueue类

我们这里新建一个名为BlockQueue的类,并声明一些属性与字段,其中ArrayDeque为一个双端队列,可以在队列的两端操作元素,size为定义的队列大小

public class BlockQueue<T> {
    // 双端队列
    private Deque<T> deque = new ArrayDeque<>();
    // 队列的容量
    private int size;

    public BlockQueue(int size) {
        this.size = size;
    }
}

2.任务的添加和获取方法

接下来我们编写向队列中添加任务与获取任务的方法,但是这里要注意的是,读写操作都是在线程池多线程的环境下进行的,存在线程安全问题,所以我们需要配合锁机制来保证操作的原子性。我们这里用JDK提供的ReentrantLock可重入锁来解决这个问题。

继续新增ReentrantLock字段:

 private ReentrantLock lock = new ReentrantLock();

此外,我们还需要解决任务添加与获取时的等待操作,也就是当队列为空时,消费者需要等待任务产生,当队列满时,生产者需要等待队列中有空位才能存入任务。如下图,当消费者获取到锁时,会尝试获取任务,但发现队列为空,就会阻塞等待。

当生产者生产任务后,肯定不能让消费者干等着,而是去通知消费者有活做啦~如下图:

我们可以看到这里涉及到了生产者与消费者不同线程的通讯,这里我们可以借助Condition来完成消费者线程与生产者线程之间的通讯。

分别定义两个Condition代表队列满情况的等待室与队列空情况的等待室

    // 队列空情况的休息室
    Condition emptyCondition = lock.newCondition();

    // 队列满情况的休息室
    Condition fullCondition = lock.newCondition();
定义阻塞添加任务方法put
// 添加任务 阻塞添加
    public void put(T task) {
        lock.lock();
        try {
            while (size == deque.size()) {
                try {
                    fullCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            log.debug("task 添加成功 ,{}", task);
            deque.addLast(task);
            emptyCondition.signal();
        } finally {
            lock.unlock();
        }
    }

这里添加任务会存在两张情况:

  • 队列满了:调用fullCondition.await()方法挂起当前生产者线程,也就是让当前生产者线程等待。
  • 队列没满:将任务加入队列中,并调用emptyCondition.signal()通知挂起的消费者。
编写堵塞拿取任务方法take
    // 获取任务
    public T take() {
        lock.lock();
        try {
            while (deque.isEmpty()) {
                try {
                    emptyCondition.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            T t = deque.removeFirst();
            fullCondition.signal();
            log.debug("获取了任务 {}", t);
            return t;
        } finally {
            lock.unlock();
        }
    }

同理,这里拿取任务也会存在两张情况:

  • 队列空的:调用emptyCondition.await()方法挂起当前消费者线程,也就是让当前消费者线程等待。
  • 队列存在元素:任务出栈,并返回出栈的任务元素,然后调用fullCondition.signal()通知挂起的生产者。

以上我们其实我们就完成了一个简单的任务堵塞队列,但是我们会发现,如果这两个方法都是会一直堵塞,显然是不合理的,所以我们这里新增添加和获取任务的超时方法。

带超时时间的阻塞添加方法offer
// 带超时时间阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while (deque.size() == size) {
                try {
                    if (nanos <= 0) {
                        return false;
                    }
                    log.debug("等待加入任务队列 {} ...", task);
                    nanos = fullCondition.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            deque.addLast(task);
            emptyCondition.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }

我们可以利用fullCondition.awaitNanos来实现超时等待,当超过给定参数时间时,就会被自动唤醒,并且将传入时间-等待时间作为返回值,下一次循环nanos <= 0时,就会判定为本次操作超时失败了

带超时时间的阻塞获取方法poll
// 带超时时间阻塞获取
    public T poll(long timeout, TimeUnit unit) {
        // 1.上锁
        lock.lock();
        try {
            long nanos = unit.toNanos(timeout); // 转为毫秒
            // 2.首先检查队列是否存在元素
            while(deque.isEmpty()){
                try {
                    // 2.1超时判断,返回值是剩余时间
                    if(nanos <= 0){
                        return null;
                    }
                    // 2.2超时等待
                    log.debug("等待获取任务");
                    nanos = emptyCondition.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 3.拿取元素
            T task = deque.removeFirst();
            log.info("任务拿取成功:{}", task);
            // 4.唤醒挂起的生产者
            fullCondition.signal();
            return task;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

总结

我们在本节内容中实现线程池中一个重要的组件 - 任务等待队列BlockQueue,编写的对应的代码也已同步到了github仓库中,估计还要两次文章的内容才能完成手写线程池部分的内容,后面主要要完成的就是线程池内部运行的基本逻辑,自定义拒绝策略等内容了,博主也是边学边实践边输出教学文章,如果有什么问题都可以在评论区留言,觉得写得不错的话就多多支持吧~

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

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

相关文章

router.push是同步还是异步?push.then和子组件onMounted谁先触发?

直接做实验&#xff1a; <el-button type"danger" click"test">toAbout</el-button>// -----const test () >{router.push(about).then((e)>{console.log(then里面, e);}).finally((e)>{console.log(finally里面, e);})console.log…

【Unity3D小功能】Unity3D中实现点击‘文字’出现‘UI面板’

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址QQ群&#xff1a;398291828 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 宠粉博主又来了&#xff0c;今天有粉丝问我如何实…

机器学习 - 神经网络分类

什么叫做分类问题&#xff1f; A classification problem involves predicting whether something is one thing or another. Problem typeWhat is it?ExampleBinary classificationTarget can be one of two options, e.g. yes or noPredict whether or not someone has hea…

AXI_Lite协议详解

文章目录 摘要一、AXI_Lite接口全局信号&#xff1a;1、写地址通道&#xff1a;2、写数据通道&#xff1a;3、写响应通道&#xff1a;4、读地址通道&#xff1a;5、读数据通道&#xff1a; 二、仿真波形 摘要 AXI4-lite是AXI4-full的简化版。用于简单、低吞吐量的内存映射通信…

基于CNN-RNN的动态手势识别系统构建与应用

一、引言 随着人机交互技术的不断发展&#xff0c;动态手势识别成为了近年来的研究热点。动态手势识别技术能够识别和理解人类的手势动作&#xff0c;从而实现更自然、更直观的人机交互。本文旨在构建一种基于CNN-RNN的动态手势识别系统&#xff0c;以提高手势识别的准确性和实…

36.基于SpringBoot + Vue实现的前后端分离-高校汉服租赁网站系统(项目 + 论文)

项目介绍 本站是一个B/S模式系统&#xff0c;采用SpringBoot Vue框架&#xff0c;MYSQL数据库设计开发&#xff0c;充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得基于SpringBoot Vue技术的高校汉服租赁网站系统设计与实现管理…

Go第三方框架--gin框架(二)

4. gin框架源码–Engine引擎和压缩前缀树的建立 讲了这么多 到标题4才开始介绍源码&#xff0c;主要原因还是想先在头脑中构建起 一个大体的框架 然后再填肉 这样不容易得脑血栓。标题四主要涉及标题2.3的步骤一 也就是 标题2.3中的 粗线框中的内容 4.1 Engine 引擎的建立 见…

Filter、Listener、AJAX

Filter 概念&#xff1a;Filter 表示过滤器&#xff0c;是JavaWeb三大组件(Servlet、Filter、 Listener)之一。 过滤器可以把对资源的请求拦截下来&#xff0c;从而实现一些特殊的功能。 过滤器一般完成一些通用的操作&#xff0c;比如&#xff1a;权限控制、统一编码处理、敏感…

Mac 装 虚拟机 vmware、centos7等

vmware&#xff1a; https://www.vmware.com/products/fusion.html centos7 清华镜像&#xff1a; 暂时没有官方的 m1 arm架构镜像 centos7 链接: https://pan.baidu.com/s/1oZw1cLyl6Uo3lAD2_FqfEw?pwdzjt4 提取码: zjt4 复制这段内容后打开百度网盘手机App&#xff0c;操…

【Python】Scrapy整合FastAPI实现爬虫API 附大量示例

文章目录 前言1. 网页分析入门1.1 基本原理1.2 Scrapy 原理 2. 创建项目2.1 创建Scrapy项目2.2.1 创建Scrapy项目2.2.2 创建Spider2.2.3 执行Demo 2.2 引入FastAPI 2. 获取Cookie3. 数据建模3.1 Scrapy 数据建模3.2 SQLAlchemy 创建实体类 3. 分析网页3.1 xpath 分析3.2 css 分…

计算机进制转换:二进制、八进制、十进制、十六进制。原码、补码、反码。

一、什么是进制 在生活中&#xff0c;我们通常都是使用阿拉伯数字计数的&#xff0c;也就是10进制&#xff0c;以10为单位&#xff0c;遇10进一&#xff0c;所以是由0&#xff0c;1&#xff0c;2、3、4、5、6、7、8、9组成的&#xff1b;而在计算机中&#xff0c;计算机是无法…

AI工具排行榜:最全工具汇总

如今,人工智能技术正在快速崛起,AI助手、语音识别、机器翻译等工具深深渗透到我们的工作和生活中。这些智能工具极大地提高了我们的工作效率,使我们能更加专注于创造性的任务。 本文将为读者推荐一些实用的AI神器,只要掌握其中一个,就能极大地提升你的工作能力,事半功倍。这些…

YOLOv9代码解读[01] readme解读

文章目录 YOLOv9COCO数据集上指标&#xff1a;环境安装训练验证重参数化 Re-parameterization推断相关链接 YOLOv9 paper: YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information github: https://github.com/WongKinYiu/yolov9 COCO数据集上指…

如何使用Python进行网络安全与密码学【第149篇—密码学】

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 用Python进行网络安全与密码学&#xff1a;技术实践指南 随着互联网的普及&#xff0c;网络…

CSS(四)

一、CSS浮动 1.1 传统网页布局的三种方式 网页布局的本质——用 CSS 来摆放盒子。 把盒子摆放到相应位置. CSS 提供了三种传统布局方式(简单说,就是盒子如何进行排列顺序)&#xff1a; 普通流&#xff08;标准流&#xff09; 浮动 定位 1.2 标准流&#xff08;普通流/文档…

【C++】哈希应用之位图

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.位图的概念 2.位…

基于React的低代码平台开发实践

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;在线地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

day07-缓存商品、购物车

1. 缓存菜品 1.1 问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大。 结果&#xff1a; 系统响应慢、用户体验差 1.2 实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询操作。 缓…

Java 在PDF中插入页眉、页脚

在处理PDF文档时&#xff0c;有时需要为文档中的每一页添加页眉和页脚&#xff0c;以包含一些有用的信息&#xff0c;如文档标题、章节名称、日期、页码等。对于需要自动化处理的场景&#xff0c;或者需要在大量文档中添加一致的页眉和页脚&#xff0c;可以通过编程的方式来实现…

java Web餐馆订单管理系统用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 JSP 餐馆订单管理系统是一套完善的web设计系统&#xff0c;对理解JSP java 编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&#xff0c;使…