吃透双端队列:并行计算的“任务分配神器”,Java实战一看就懂

吃透双端队列:并行计算的“任务分配神器”,Java实战一看就懂

做Java开发,我们几乎天天和各类集合容器打交道:靠着连续内存的优势,随机查询速度快到飞起;凭借双向指针串联java fifo队列,首尾插删效率拉满。可一旦遇到两端高频操作+兼顾性能的场景,比如并行任务调度、缓存数据淘汰,这两款常用容器就会暴露短板,显得力不从心。

而双端队列(Deque)恰好补上了这个技术空白,完美解决上述痛点——它既能实现队列“先进先出”的特性,也能兼容栈“后进先出”的功能,首尾插删操作全是O(1)高效效率,更是Java并行计算中,工作窃取算法不可或缺的核心底层组件。

本文全程抛开晦涩源码术语,用通俗语言讲透双端队列底层原理,对比、理清选型逻辑,搭配3组可直接复制运行的Java实战代码,新手也能轻松吃透,干货拉满建议收藏~

一、先搞懂:为啥需要双端队列?数组与链表的场景短板

想要吃透双端队列,先看清和在“两端操作”场景下的硬伤,就能明白它的诞生意义,绝非多余的容器。

1. 动态数组():头部操作是致命短板

依托连续内存空间存储数据,随机访问效率极高,时间复杂度O(1),但头部插删堪称“灾难”:

在数组头部执行添加或删除操作时,后续所有元素都要整体向后或向前移位,数据量越大,移位耗时越长,时间复杂度直接飙升至O(N);即便尾部插删效率尚可,也完全无法适配两端高频操作的业务场景。

2. 双向链表():内存开销大、访问低效

靠双向指针串联节点,首尾插删确实是O(1)效率,不用挪动元素,但短板同样明显:

每个节点都要额外存储前驱、后继双向指针,内存开销远大于普通数组;再加上非连续内存的特性,想要随机访问某个元素,必须从头节点或尾节点开始遍历,效率跌至O(N),没法兼顾“两端高效操作”和“快速随机访问”,整体性能损耗较大。

3. 双端队列(Deque):集两家之长的最优解

双端队列可以理解为“优化版首尾操作容器”,完美规避前两者的短板,核心优势拉满:

通俗比喻:是一排固定连号座位,头部加人全排挪动;是零散手拉手的人群,定位费劲;双端队列就是环形首尾相接的座位,既能从车头上下人,也能从车尾上下人,不用挪动其他位置,灵活又高效。

二、双端队列核心原理:严谨通俗,新手也能看懂

Java中Deque是双端队列接口,主流实现为(循环数组,非线程安全,性能首选)、(双向链表,线程安全),其中是日常开发首选,底层逻辑简单易懂,且无虚耗。

1. 底层核心:循环数组,高效无虚耗

摒弃了普通数组的单向存储,采用循环数组结构,搭配双指针实现首尾操作,这是它高效的核心:

核心运转逻辑:通过取余运算实现数组首尾相连,形成环形结构,头部插删时只需前移head指针java fifo队列,尾部插删时只需后移tail指针吃透双端队列:并行计算的“任务分配神器”,Java实战一看就懂,全程不用挪动中间任何元素,操作效率直接拉满;当数组空间不足时,仅需扩容数组、有序拷贝数据,扩容成本远低于吃透双端队列:并行计算的“任务分配神器”,Java实战一看就懂,性能损耗极低。

2. 核心特性:一器两用,双模式切换

双端队列的“双端”不只是首尾操作,更能无缝切换两种数据结构模式,开发中直接套用即可:

3. 扩容逻辑:智能扩容,避免内存浪费

默认初始容量为16,扩容逻辑比更智能,全程无多余操作:

队列满员判定:head指针与tail指针重合,即代表数组空间耗尽;扩容规则:容量不足时,直接扩容为原容量的2倍,将原有数据有序拷贝至新数组,重置head、tail指针;空间优化:不会主动缩容,避免频繁扩容缩容带来的性能损耗,适合绝大多数业务场景。4. 三大容器核心对比:选型不踩坑

核心特性

(动态数组)

(双向链表)

(双端队列)

存储结构

连续内存

双向节点+指针

循环数组

首尾插删效率

头部O(N)、尾部O(1)

首尾均O(1)

首尾均O(1)

随机访问效率

O(1),超快

O(N),需遍历

O(1),高效

内存开销

极低java fifo队列,无额外指针

高,节点自带双指针

低,仅数组+双指针

线程安全

非安全

非安全

非安全

适用场景

高频查询、少量插删

高频首尾插删、无查询

高频两端操作、兼顾查询

三、Java实战代码:可运行,吃透核心用法

以下三组代码均为独立原创,注释详尽、逻辑清晰,复制到IDE即可直接运行,覆盖底层实现、原生API、实战应用,彻底吃透双端队列。

案例1:简易双端队列实现(吃透底层逻辑)

基于循环数组手写简易双端队列,剔除复杂源码细节,聚焦首尾插删、扩容核心逻辑,新手入门必看,直观理解底层运转机制。

/**
 *  简易双端队列
 * 基于循环数组实现,吃透首尾操作+扩容核心
 */
public class SimpleArrayDeque {
    // 存储数据的数组
    private int[] elementData;
    // 队头指针:指向首个有效元素
    private int head;
    // 队尾指针:指向尾部可插入空位
    private int tail;
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 16;
    // 构造方法:初始化默认容量
    public SimpleArrayDeque() {
        elementData = new int[DEFAULT_CAPACITY];
        head = tail = 0;
    }
    // 头部添加元素
    public void addFirst(int val) {
        // 指针前移,取余实现循环
        head = (head - 1 + elementData.length) % elementData.length;
        elementData[head] = val;
        // 队列满员,触发扩容
        if (head == tail) {
            resize();
        }
    }
    // 尾部添加元素
    public void addLast(int val) {
        elementData[tail] = val;
        // 指针后移,取余实现循环
        tail = (tail + 1) % elementData.length;
        // 队列满员,触发扩容
        if (head == tail) {
            resize();
        }
    }
    // 头部删除元素
    public int removeFirst() {
        if (head == tail) {
            throw new RuntimeException("双端队列为空,无法删除");
        }
        int res = elementData[head];
        // 释放引用,助力GC
        elementData[head] = 0;
        head = (head + 1) % elementData.length;
        return res;
    }
    // 尾部删除元素
    public int removeLast() {
        if (head == tail) {
            throw new RuntimeException("双端队列为空,无法删除");
        }
        // 指针前移
        tail = (tail - 1 + elementData.length) % elementData.length;
        int res = elementData[tail];
        // 释放引用,助力GC
        elementData[tail] = 0;
        return res;
    }
    // 扩容方法:扩容为原容量2倍
    private void resize() {
        int oldLen = elementData.length;
        // 新容量=旧容量*2
        int newLen = oldLen << 1;
        int[] newData = new int[newLen];
        // 拷贝原有数据
        for (int i = 0; i < oldLen; i++) {
            newData[i] = elementData[(head + i) % oldLen];
        }
        elementData = newData;
        // 重置指针
        head = 0;
        tail = oldLen;
    }
    // 测试核心功能
    public static void main(String[] args) {
        SimpleArrayDeque deque = new SimpleArrayDeque();
        // 尾部添加
        deque.addLast(1);
        deque.addLast(2);
        deque.addLast(3);
        // 头部添加
        deque.addFirst(0);
        System.out.println("头部删除:" + deque.removeFirst()); // 0
        System.out.println("尾部删除:" + deque.removeLast());  // 3
    }
}

案例2:Java原生Deque实战(开发直接用)

Java官方高频API,区分队列、栈双模式用法,贴合企业实际开发,拿来就能用,替代低效容器。

   import java.util.ArrayDeque;
import java.util.Deque;
/**
 * 原生ArrayDeque实战
 * 队列+栈双模式用法,企业开发首选
 */
public class DequeApiTest {
    public static void main(String[] args) {
        // 初始化双端队列,默认容量16
        Deque deque = new ArrayDeque();
        // ========== 1. 队列模式:先进先出 FIFO ==========
        System.out.println("===== 队列模式 =====");
        // 尾部入队
        deque.offerLast("任务001");
        deque.offerLast("任务002");
        deque.offerLast("任务003");
        System.out.println("队列初始数据:" + deque);
        // 头部出队
        System.out.println("出队元素:" + deque.pollFirst());
        System.out.println("队列剩余数据:" + deque);
        // ========== 2. 栈模式:后进先出 LIFO ==========
        System.out.println("n===== 栈模式 =====");
        // 头部入栈
        deque.push("任务004");
        deque.push("任务005");
        System.out.println("栈内数据:" + deque);
        // 头部出栈
        System.out.println("出栈元素:" + deque.pop());
        System.out.println("栈剩余数据:" + deque);
        // ========== 3. 常用查询操作 ==========
        System.out.println("n===== 常用查询 =====");
        // 获取队头(不删除)
        System.out.println("队头元素:" + deque.peekFirst());
        // 获取队尾(不删除)
        System.out.println("队尾元素:" + deque.peekLast());
        // 清空队列
        deque.clear();
        System.out.println("队列是否为空:" + deque.isEmpty());
    }
}

案例3:工作窃取算法模拟(双端队列核心应用)

双端队列最经典的实战场景——工作窃取算法,是Java 并行计算的核心,利用两端操作避免线程竞争,大幅提升并行效率。

 import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Random;
/**
 *  工作窃取算法模拟
 * 双端队列核心实战,理解并行任务调度
 */
public class WorkStealTest {
    // 工作线程:处理自身任务,空闲则窃取其他线程任务
    private static class WorkThread extends Thread {
        private final Deque taskQueue;
        private final WorkThread[] allThreads;
        private final Random random = new Random();
        public WorkThread(Deque taskQueue, WorkThread[] allThreads) {
            this.taskQueue = taskQueue;
            this.allThreads = allThreads;
        }
        @Override
        public void run() {
            while (true) {
                // 1. 优先处理自身队列任务(头部获取)
                Integer task = taskQueue.pollFirst();
                if (task != null) {
                    handleTask(task);
                } else {
                    // 2. 自身无任务,窃取其他线程尾部任务(避免竞争)
                    task = stealTask();
                    if (task == null) {
                        // 无任务可窃取,退出线程
                        break;
                    }
                    handleTask(task);
                }
            }
        }
        // 处理任务:模拟业务耗时
        private void handleTask(int taskId) {
            System.out.println(Thread.currentThread().getName() + " 处理任务:" + taskId);
            try {
                Thread.sleep(random.nextInt(50));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        // 窃取任务:从其他线程队列尾部获取
        private Integer stealTask() {
            for (WorkThread thread : allThreads) {
                if (thread != this && !thread.taskQueue.isEmpty()) {
                    synchronized (thread.taskQueue) {
                        if (!thread.taskQueue.isEmpty()) {
                            return thread.taskQueue.pollLast();
                        }
                    }
                }
            }
            return null;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 初始化3个工作线程
        int threadNum = 3;
        WorkThread[] threads = new WorkThread[threadNum];
        Deque[] taskQueues = new ArrayDeque[threadNum];
        // 为每个线程分配初始任务
        for (int i = 0; i < threadNum; i++) {
            taskQueues[i] = new ArrayDeque();
            // 每个线程分配5个任务
            for (int j = 0; j < 5; j++) {
                taskQueues[i].offerLast(i * 10 + j);
            }
            threads[i] = new WorkThread(taskQueues[i], threads);
            threads[i].setName("工作线程-" + i);
        }
        // 启动所有线程
        for (WorkThread thread : threads) {
            thread.start();
        }
        // 等待所有线程执行完毕
        for (WorkThread thread : threads) {
            thread.join();
        }
        System.out.println("n===== 所有任务处理完成 =====");
    }
}

四、双端队列经典应用场景,开发直接套用并行计算任务调度:底层依托双端队列实现工作窃取,是多核并行计算的核心;栈/队列替代:替代性能拉胯的Stack类、内存虚耗的,一器两用;滑动窗口算法:处理字符串/数组滑动窗口问题,首尾增删元素高效,优化算法时间复杂度;缓存淘汰策略:实现LRU、LFU缓存,快速淘汰、新增缓存数据;浏览器/编辑器历史记录:前进、后退操作,依托两端操作实现快速切换。五、双端队列使用避坑指南,避开90%常见问题

双端队列(Deque)是Java集合容器中被低估的“效率神器”,它依托循环数组实现首尾O(1)高效操作,平衡了内存占用与访问性能,既解决了头部操作慢的短板,又弥补了内存开销大的缺陷。

开发中牢记选型准则:高频查询选,高频首尾插删选,两端高频操作+兼顾性能选,并发场景则选用。

公告:
1. 本站所有资源来源于用户上传和网络,如有侵权请联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系站长处理!
6. 本站不售卖代码,资源标价只是站长收集整理的辛苦费!如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
7. 站长QQ号码 2205675299

资源库 - 资源分享下载网 » 吃透双端队列:并行计算的“任务分配神器”,Java实战一看就懂

常见问题FAQ

关于资源售价和售后服务的说明?
本站所有资源的标价均为本站收集资源的辛苦费,不代表资源本身的价值。软件是高智慧高价值的商品,不可能是白菜价。本站资源标价只是赞助费用,收取的赞助费仅用来维持本站的日常运营!毕竟收集整理资料需要投入云计算资源和站长大量的精力。
代码有没有售后服务和技术支持?
由于代码的运行具有不可预见性,本站不保证代码完整可运行,不提供技术支持和售后服务。 本站原创代码都是站长自己开发的,可以有偿提供技术支持服务。 网站里标明【亲测】的代码都是站长亲测过的,其他的代码由于精力有限,没有一一测试,不能保证代码就一定能够使用,更没有技术支持服务,下载前请自行斟酌。
有没有搭建服务?
由于搭建服务比较费时费力,所以本站除了原创代码外均不提供搭建服务。本站分享代码纯属兴趣爱好,不以盈利为目的,请勿咨询有没有搭建服务,谢谢理解。
链接地址失效了怎么办?
请带上资源链接地址联系客服,工作时间内我们看到后将第一时间回复。
关于解压密码
本站资源一般都没有加密,如果发现需要解压密码的,那么就输入 hao.35dc.com 试试。

发表评论

资源库,由老程序员细心甄别、精心筛选,只为提供优质的源码资源

关于我们 联系我们