【026】面试被问阻塞队列?王二写的订单系统丢单,哇哥 1 行代码用 ArrayBlockingQueue 救活!


请添加图片描述

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta

零、引入

请添加图片描述

“订单又丢了!用户付了钱,系统没下单,客服电话被打爆了!” 王二的哀嚎响彻技术部,他负责的订单系统上线后,要么因为任务堆积 OOM 卡死,要么因为队列满了直接拒绝任务,光用户投诉就攒了上百条。领导把投诉单拍在他桌上,指节咔咔响:“再搞不定,这个月绩效清零,还得赔用户损失!”

王二的代码里,线程池要么用无界的 LinkedBlockingQueue,高峰期任务堆成山,内存直接炸了;换成有界 ArrayBlockingQueue,又因为 RejectedExecutionException 丢单 —— 用户付了钱,系统直接提示 “繁忙”,订单凭空消失。隔壁哇哥端着泡满枸杞的保温杯过来,扫了眼代码就乐了:“你这是没搞懂阻塞队列的核心 —— 普通队列满了就拒,空了就返回 null;阻塞队列满了会等,空了也会等,正好解决你的丢单和 OOM 问题。今天把这事儿搞懂,不仅能救你的绩效,下次面试被问阻塞队列,你能把面试官说懵。”

点赞 + 关注跟着哇哥和王二,用食堂打饭的例子搞懂阻塞队列,代码直接抄,再也不怕订单丢了、服务器炸了!

一、王二的 “丢单代码”:线程池队列的两个致命坑

王二的订单处理逻辑很简单:用户下单请求过来,线程池处理 “查库存→扣余额→写订单” 的流程。单测时顺风顺水,一到高峰期就出问题 —— 要么 OOM,要么丢单。

🥰 王二的 “自杀式” 订单代码

请添加图片描述

package cn.tcmeta.blockingqueue;

import java.util.concurrent.*;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: “自杀式” 订单代码
 * @version: 1.0.0
 */
// 订单系统——丢单+OOM版
public class OrderSystemBug {
    // 线程池配置:核心5线程,最大10线程,有界队列100
    static ExecutorService pool = new ThreadPoolExecutor(
            5, 10,
            60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100), // 有界队列
            new ThreadPoolExecutor.AbortPolicy() // 队列满了直接拒绝任务
    );

    // 模拟下单任务
    record OrderTask(int orderId) implements Runnable {
        @Override
        public void run() {
            // 模拟订单处理耗时(查库存、扣余额、写日志)
            try {
                sleep(100);
                System.out.println("处理订单:" + orderId + ",线程:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 模拟高峰期:1000个下单请求
        for (int i = 1; i <= 1000; i++) {
            try {
                pool.submit(new OrderTask(i));
            } catch (RejectedExecutionException e) {
                // 队列满了,任务被拒绝——订单丢了!
                System.out.println("订单" + i + "处理失败:系统繁忙(用户付了钱没下单!)");
            }
        }
        pool.shutdown();
    }
}

执行结果:

订单111处理失败:系统繁忙(用户付了钱没下单!)
订单112处理失败:系统繁忙(用户付了钱没下单!)
订单113处理失败:系统繁忙(用户付了钱没下单!)
订单114处理失败:系统繁忙(用户付了钱没下单!)
订单115处理失败:系统繁忙(用户付了钱没下单!)
订单116处理失败:系统繁忙(用户付了钱没下单!)
订单117处理失败:系统繁忙(用户付了钱没下单!)
订单118处理失败:系统繁忙(用户付了钱没下单!)
订单119处理失败:系统繁忙(用户付了钱没下单!)
订单120处理失败:系统繁忙(用户付了钱没下单!)
订单121处理失败:系统繁忙(用户付了钱没下单!)
订单122处理失败:系统繁忙(用户付了钱没下单!)
订单123处理失败:系统繁忙(用户付了钱没下单!)
订单124处理失败:系统繁忙(用户付了钱没下单!)
订单125处理失败:系统繁忙(用户付了钱没下单!)
订单126处理失败:系统繁忙(用户付了钱没下单!)
订单127处理失败:系统繁忙(用户付了钱没下单!)

运行结果:用户钱付了,订单没了
控制台满屏 “订单 XXX 处理失败”,用户在 APP 上显示 “支付成功”,但商家后台没订单,客服电话被打爆 —— 这就是典型的 “队列满了直接拒绝” 导致的丢单问题;如果把队列换成无界的 LinkedBlockingQueue,又会因为任务无限堆积导致 OOM,服务器直接卡死。

二、用食堂打饭,讲透阻塞队列的核心逻辑

“你把线程池的队列想成食堂的打饭窗口,” 哇哥拿王二的午饭举例子,“普通队列就像窗口贴了‘满 3 人就拒’,来第 4 个人直接赶跑(丢单);无界队列像窗口无限排队,人堆到食堂门口,挤爆大门(OOM)。而阻塞队列是‘人性化窗口’:

  • 窗口满了(队列满),新来的人不跑,站在后面等(线程阻塞),直到有人打完饭有空位;
  • 窗口没人(队列空),打饭阿姨也不闲着,等着来人(消费线程阻塞),直到有新的人来打饭。”

✔️ 阻塞队列的核心特性(人话版)

在这里插入图片描述
“阻塞队列的 put () 和 take () 是核心 —— 这两个方法会‘等’,而不是‘拒’或‘抛异常’,完美解决你的丢单和 OOM 问题。”

三、第一招:用 ArrayBlockingQueue 改造线程池,丢单 + OOM 全解决

哇哥教王二改了两行代码:把线程池的拒绝策略换成 CallerRunsPolicy(队列满了提交线程自己执行,不丢单),队列用有界阻塞队列控制容量(防 OOM)。

🥰 修复后的订单系统(无丢单 + 无 OOM)

在这里插入图片描述

package cn.tcmeta.blockingqueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: // 订单系统——阻塞队列修复版
 * @version: 1.0.0
 */
public class OrderSystemFixed {
    // 核心改造:阻塞队列+合理拒绝策略
    static ExecutorService pool = new ThreadPoolExecutor(
            5,
            10, // 核心5线程,最大10线程
            60,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100), // 有界阻塞队列,控制容量防OOM
            // 拒绝策略:队列满了,提交任务的线程自己执行,不丢单
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    record OrderTask(int orderId) implements Runnable {
        @Override
        public void run() {
            try {
                sleep(100); // 模拟业务耗时
                System.out.println("处理订单:" + orderId + ",线程:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 模拟1000个下单请求
        for (int i = 1; i <= 1000; i++) {
            pool.submit(new OrderTask(i)); // 不会丢单,队列满了提交线程自己执行
        }

        // 等待所有任务处理完成
        pool.shutdown();
        try {
            pool.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("所有订单处理完成,无丢单!");
    }
}

执行结果:

处理订单:111,线程:main
处理订单:3,线程:pool-1-thread-3
处理订单:107,线程:pool-1-thread-7
处理订单:4,线程:pool-1-thread-4
处理订单:5,线程:pool-1-thread-5
处理订单:106,线程:pool-1-thread-6
处理订单:2,线程:pool-1-thread-2
处理订单:1,线程:pool-1-thread-1
处理订单:110,线程:pool-1-thread-10
处理订单:108,线程:pool-1-thread-8
处理订单:109,线程:pool-1-thread-9
处理订单:11,线程:pool-1-thread-2

运行效果:订单全处理,内存稳如狗

王二跑起来发现,控制台再也没有 “订单处理失败” 的日志,所有 1000 个订单都被处理了,内存占用只有之前的 1/3—— 阻塞队列既限制了任务堆积(防 OOM),又通过阻塞和合理的拒绝策略避免了丢单。

📢 阻塞队列基础用法(新手必看)

为了让王二彻底理解,哇哥写了段极简代码,展示 ArrayBlockingQueue 的 put () 和 take ()

package cn.tcmeta.blockingqueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: // 阻塞队列基础:ArrayBlockingQueue
 * @version: 1.0.0
 */
public class BlockingQueueBasic {
    public static void main(String[] args) throws InterruptedException {
        // 创建有界阻塞队列:容量3
        ArrayBlockingQueue<String> orderQueue = new ArrayBlockingQueue<>(3);

        // 生产者线程:放订单,满了就等
        new Thread(() -> {
            try {
                orderQueue.put("订单1");
                orderQueue.put("订单2");
                orderQueue.put("订单3");
                System.out.println("生产者:队列满了,等位置放订单4...");
                orderQueue.put("订单4"); // 阻塞,直到消费者取走订单
                System.out.println("生产者:订单4放入成功!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者").start();

        // 等2秒,让生产者放满3个订单
        Thread.sleep(2000);

        // 消费者线程:取订单,空了就等
        new Thread(() -> {
            try {
                System.out.println("消费者:取走" + orderQueue.take());
                System.out.println("消费者:取走" + orderQueue.take());
                // 取走2个,队列剩1个,生产者的订单4可以放入了
                TimeUnit.SECONDS.sleep(1);
                System.out.println("消费者:取走" + orderQueue.take());
                System.out.println("消费者:队列空了,等新订单...");
                orderQueue.take(); // 阻塞,直到有新订单
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者").start();
    }
}

执行结果:

生产者:队列满了,等位置放订单4...
生产者:订单4放入成功!
消费者:取走订单1
消费者:取走订单2
消费者:取走订单3
消费者:队列空了,等新订单...

运行结果清晰展示了 “满了等、空了等” 的核心逻辑,王二一眼就懂了。

四、阻塞队列的核心优势(王二记满 3 页笔记)

王二掏出小本本,把哇哥的话记成 “傻瓜式总结”:

  • 防 OOM:有界阻塞队列限制容量,任务不会无限堆积,内存稳如老狗;
  • 防丢单:put () 阻塞等待,配合 CallerRunsPolicy 拒绝策略,任务不会被直接拒绝;
  • 解耦:生产者(下单请求)和消费者(订单处理)解耦,不用直接交互;
  • 简单:JDK 自带,不用自己写 wait/notify 的阻塞逻辑,避免死锁。

💯 哇哥的血泪彩蛋

“我刚工作时,自己用 wait/notify 写阻塞逻辑,” 哇哥捂脸,“结果写死锁了,把测试库搞崩了,被 DBA 追着骂 —— 后来用阻塞队列,再也没翻过车,现成的轮子不香吗?”

‼️ 下一篇预告

王二用 ArrayBlockingQueue 解决了丢单问题,但新问题又来了:“秒杀系统用这个队列,订单堆积在队列里,响应慢成龟速!” 哇哥说这是 “队列选型错了”—— 下一篇我们扒透:

  • SynchronousQueue:无存储队列,秒杀系统 QPS 翻倍的秘密;
  • PriorityBlockingQueue:VIP 订单优先处理的玩法;
  • DelayQueue:订单超时未支付自动取消的实现;

在这里插入图片描述
请添加图片描述
在这里插入图片描述
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值