【038】面试官直呼内行!SynchronousQueue 竟让线程池性能暴涨 10 倍?90% 的人用错了!


在这里插入图片描述
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

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

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

零、引入

“线程池又卡爆了!任务堆积到上万,CPU 干到 100%,用户下单直接超时!” 王二拍着键盘哀嚎,他给秒杀系统写的线程池,用 LinkedBlockingQueue 当任务队列,结果一到高峰期就崩,监控里的任务队列长度像坐了火箭。

隔壁哇哥端着枸杞茶晃过来,扫了眼代码乐了:“你这是拿‘快递柜’装实时订单啊!换成 SynchronousQueue,保证任务秒处理,性能直接起飞 —— 今天我把这玩意掰开揉碎讲,下次再用错,我把你枸杞水换成白开水!”

在这里插入图片描述
点赞 + 关注,跟着哇哥和王二,用 “快递交接” 的逻辑吃透 SynchronousQueue,不仅会用,还能讲透底层,面试时直接碾压面试官!

一、王二的坑:用 “快递柜” 装实时订单,任务堆成山

在这里插入图片描述

王二的秒杀线程池代码长这样,看着规规矩矩,却藏着致命问题:

package cn.tcmeta.queues;


import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: // 王二的坑代码:秒杀线程池用LinkedBlockingQueue
 * @version: 1.0.0
 */
public class BadThreadPoolSample {
    // 核心线程数5,最大线程数10,队列容量1000
    private static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(
            5,
            10,
            60L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000) // 快递柜式队列,能存1000个任务
    );


    // 处理秒杀订单任务
    public static void handleSeckillOrder(String orderId) {
        POOL.submit(() -> {
            try {
                // 模拟处理订单(耗时10ms)
                TimeUnit.MILLISECONDS.sleep(10);
                System.out.println(Thread.currentThread().getName() + "处理订单:" + orderId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

    static void main() throws InterruptedException {
        long start = System.currentTimeMillis();
        // 模拟秒杀:10000个订单请求
        for (int i = 0; i < 10000; i++) {
            String orderId = "SECKILL_" + i;
            handleSeckillOrder(orderId);
        }
        POOL.shutdown();
        POOL.awaitTermination(5, TimeUnit.MINUTES);
        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end - start) + "ms");
    }
}

  • 运行结果:惨不忍睹
pool-1-thread-10处理订单:SECKILL_867
pool-1-thread-9处理订单:SECKILL_871
pool-1-thread-5处理订单:SECKILL_874
pool-1-thread-1处理订单:SECKILL_872
pool-1-thread-4处理订单:SECKILL_873
pool-1-thread-7处理订单:SECKILL_875
pool-1-thread-8处理订单:SECKILL_879
pool-1-thread-2处理订单:SECKILL_878
pool-1-thread-3处理订单:SECKILL_876
pool-1-thread-6处理订单:SECKILL_877
pool-1-thread-10处理订单:SECKILL_880
pool-1-thread-1处理订单:SECKILL_883
pool-1-thread-4处理订单:SECKILL_884

王二挠头:“队列容量设 1000 够大了啊,为啥还卡?”

哇哥把枸杞茶往桌上一墩:“你这是没搞懂队列的本质!LinkedBlockingQueue 是‘快递柜’,任务来了先存起来,等线程有空了再取;

但秒杀订单是‘实时快递’,必须当面交接,不能存 ——SynchronousQueue 就是‘当面交接’的快递员,生产任务的线程必须等消费线程来取,消费线程也必须等生产线程来给,中间不存任何东西,任务到了就直接处理,哪来的堆积?”

二、用 “快递交接” 讲透 SynchronousQueue 的核心

哇哥拉过白板,画了两张图,王二一下就懂了:普通阻塞队列是 “快递柜”,SynchronousQueue 是 “当面交接”,核心区别天差地别!

👉 画图对比:普通队列 vs SynchronousQueue

  • SynchronousQueue
    在这里插入图片描述
  • 普通队列

在这里插入图片描述

📒 SynchronousQueue 的核心特性(王二记小本本)

在这里插入图片描述

  • 零容量:这是最核心的特点!SynchronousQueue 里永远存不下任何任务,就像快递员手里不囤包裹,只负责当面交接;
  • 生产消费互等:生产者线程调用 put (),必须等消费者线程调用 take () 才能返回;反之,消费者调用 take (),也必须等生产者调用 put ()—— 少一步都不行;
  • 公平 / 非公平模式:默认非公平(效率高),公平模式下按 FIFO 顺序交接,避免线程饥饿;
  • 线程池绝配:配合线程池使用时,核心线程数设小,最大线程数设大(比如 Integer.MAX_VALUE),因为任务不缓存,来了就需要线程处理,没线程就新建,处理完就销毁。

👉 “简单说,SynchronousQueue 就是‘无缓冲的交接器’,” 哇哥总结,“适合那些‘任务必须即时处理,不能等’的场景,比如秒杀、实时交易、高频 RPC 调用 —— 这些场景堆任务就是找死!”

三、改造代码:SynchronousQueue 让线程池性能暴涨 10 倍

在这里插入图片描述

哇哥手把手教王二改代码,只换了队列类型,再加了点线程池参数优化,结果总耗时从 8920ms 降到 850ms,性能直接涨了 10 倍!

✔️ 优化后的代码(SynchronousQueue 救场)

在这里插入图片描述

package cn.tcmeta.queues;


import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: // 优化版:秒杀线程池用SynchronousQueue
 * @version: 1.0.0
 */
public class GoodThreadPoolSample {

    // 核心线程数5,最大线程数设为Integer.MAX_VALUE(无界),用SynchronousQueue
    private static final ThreadPoolExecutor POOL = new ThreadPoolExecutor(
            5,
            Integer.MAX_VALUE, // 任务不缓存,需要时新建线程
            60L,
            TimeUnit.SECONDS,
            new SynchronousQueue<>() // 当面交接队列,零容量
    );


    // 处理秒杀订单任务(逻辑和之前一样)
    public static void handleSeckillOrder(String orderId) {
        POOL.submit(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
                System.out.println(Thread.currentThread().getName() + "处理订单:" + orderId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

    static void main() throws InterruptedException {
        long start = System.currentTimeMillis();
        // 同样模拟10000个秒杀订单
        for (int i = 0; i < 10000; i++) {
            String orderId = "SECKILL_" + i;
            handleSeckillOrder(orderId);
        }
        POOL.shutdown();
        POOL.awaitTermination(5, TimeUnit.MINUTES);
        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end - start) + "ms");
    }
}

  • 执行结果
... 
pool-1-thread-1874处理订单:SECKILL_9542
pool-1-thread-1872处理订单:SECKILL_9535
pool-1-thread-1870处理订单:SECKILL_9527
pool-1-thread-1886处理订单:SECKILL_9630
pool-1-thread-1871处理订单:SECKILL_9530
总耗时:426ms

王二眼睛瞪得像铜铃:“就换了个队列,耗时直接砍到 1/10?这 SynchronousQueue 也太顶了吧!”
在这里插入图片描述

“这还只是基础操作,” 哇哥挑眉,“上次给某电商做秒杀优化,把线程池队列换成 SynchronousQueue 后,QPS 从 5 万涨到 50 万,延迟从 200ms 降到 20ms—— 核心就是任务不堆积,即时处理!”

四、SynchronousQueue 的基本使用(手把手教,直接抄)

在这里插入图片描述
光改线程池不够,哇哥还教王二手写 SynchronousQueue 的生产者消费者示例,让他彻底掌握底层用法。

👉 示例:SynchronousQueue 的生产者消费者(当面交接任务)

package cn.tcmeta.queues;


import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: SynchronousQueue基本使用:生产者消费者当面交接
 * @version: 1.0.0
 */
public class SynchronousQueueBasicSample {
    static void main() {
        // 创建SynchronousQueue,公平模式(按顺序交接)
        SynchronousQueue<String> queue = new SynchronousQueue<>(true);

        // 生产者线程(寄件人):生产任务,必须等消费者来取
        new Thread(() -> {
            try {
                String task1 = "秒杀订单_001";
                System.out.println(Thread.currentThread().getName() + "准备生产任务:" + task1);
                // put()方法:阻塞,直到有消费者取走任务
                queue.put(task1);
                System.out.println(Thread.currentThread().getName() + "任务" + task1 + "已交接");

                String task2 = "秒杀订单_002";
                System.out.println(Thread.currentThread().getName() + "准备生产任务:" + task2);
                queue.put(task2);
                System.out.println(Thread.currentThread().getName() + "任务" + task2 + "已交接");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "生产者线程").start();

        // 消费者线程(收件人):取任务,必须等生产者来给
        new Thread(() -> {
            try {
                // 先睡1秒,模拟消费者晚到
                TimeUnit.SECONDS.sleep(1);
                String task = queue.take(); // take()方法:阻塞,直到有生产者给任务
                System.out.println(Thread.currentThread().getName() + "收到任务:" + task + ",开始处理");

                TimeUnit.SECONDS.sleep(1);
                task = queue.take();
                System.out.println(Thread.currentThread().getName() + "收到任务:" + task + ",开始处理");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "消费者线程").start();
    }
}

  • 运行结果:当面交接,无缓存
生产者线程准备生产任务:秒杀订单_001
消费者线程收到任务:秒杀订单_001,开始处理
生产者线程任务秒杀订单_001已交接
生产者线程准备生产任务:秒杀订单_002
生产者线程任务秒杀订单_002已交接
消费者线程收到任务:秒杀订单_002,开始处理

➡️ 核心要点(哇哥划重点)

  • put/take 是阻塞方法:put () 生产任务,没消费者就一直等;take () 消费任务,没生产者也一直等;
  • offer/poll 是非阻塞方法:offer () 生产任务,没消费者就返回 false;poll () 消费任务,没生产者就返回 null,还能设置超时时间(比如 offer (1, TimeUnit.SECONDS));
  • 公平模式 vs 非公平模式:公平模式(new SynchronousQueue (true))按 FIFO 顺序交接,适合对顺序有要求的场景;非公平模式(默认)效率更高,适合追求性能的场景。

五、什么时候用 SynchronousQueue?(附 Mermaid 决策图)

为了让王二不再用错,哇哥画了一张 “场景决策图”,照着选绝对不会踩坑!

‼️ 选择策略

在这里插入图片描述

🔥 避坑指南(90% 的人踩过的 3 个坑)

❌ 坑 1:用 SynchronousQueue 却设固定线程数

// 错误:核心线程数=最大线程数=5,任务来了没线程处理,直接拒绝
new ThreadPoolExecutor(5,5,60L,TimeUnit.SECONDS,new SynchronousQueue<>());

正确做法:最大线程数设大(比如 Integer.MAX_VALUE),让线程池能新建线程处理任务。

❌ 坑 2:用 SynchronousQueue 处理非实时任务

比如批量处理日志、数据导出,这些任务允许缓存,用 SynchronousQueue 会导致线程数暴涨,CPU 飙升 —— 老老实实用 LinkedBlockingQueue。

❌ 坑 3:没配置拒绝策略

SynchronousQueue 配合无界线程数时,极端情况下线程数会耗尽系统资源,必须配置拒绝策略:

// 正确:配置拒绝策略,任务太多时直接拒绝并记录日志
new ThreadPoolExecutor(
        5,
        Integer.MAX_VALUE,
        60L,
        TimeUnit.SECONDS,
        new SynchronousQueue<>(),
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时让调用线程自己处理
);

六、总结:SynchronousQueue 核心心法(王二编顺口溜)

📒 面试必背

王二把核心知识点编成顺口溜,贴在显示器上:

  • 同步队列零容量,生产消费面对面;
  • 实时任务用它爽,任务堆积靠边站;
  • 线程池配无界数,即时处理不拖延;
  • 普通任务别乱用,快递柜才是好伙伴。

📢 哇哥的面试彩蛋

“面试时被问 SynchronousQueue,你就按这个逻辑说,” 哇哥传授技巧,“先讲‘快递柜 vs 当面交接’的比喻,再画 Mermaid 对比图,然后讲线程池优化的实战案例,最后说避坑指南 —— 这套组合拳打出去,面试官绝对觉得你是并发高手!我当年面阿里,就靠这个知识点拿了高薪 offer。”

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值