JUC 在中间件中的应用

JUC 在中间件中的应用:线程池与并发集合实战全景 问题切入:道格·李的组件在中间件里是如何落地的 道格·李设计的每一个 JUC 组件都有明确的定位:ThreadPoolExecutor 管理线程资源、ConcurrentHashMap 提供高并发下的安全容器、BlockingQueue 协调生产者与消费者。但这些组件本身只是"积木"——积木搭成什么,看用的人。 Tomcat、Netty、Dubbo、RocketMQ 这些中间件的作者,就是最高水平的积木搭手。他们在道格·李提供的基础上做了大量二次定制:继承 ThreadPoolExecutor 改写拒绝策略、用 ConcurrentHashMap 存储单例对象、用 BlockingQueue 实现异步日志缓冲。 翻开这些中间件的源码,你会发现:标准 JUC 组件很少被直接使用,几乎都被继承或组合包装。这不是因为标准组件不够好,而是因为每个中间件的场景都有自己的约束——Tomcat 的线程池需要在队列满时反过来创建线程(而不是拒绝),Netty 用 NioEventLoopGroup 把线程池拆成了事件循环。 本篇从源码层面逐一拆解道格·李的 JUC 积木如何在中间件中被定制、组合和落地。覆盖的中间件和对应的 JUC 组件如下: flowchart LR classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold; classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; ROOT[JUC 在中间件中的应用全景] ROOT --> THREAD["线程池 ThreadPoolExecutor"] THREAD --> T1["Tomcat: 请求处理线程池\n自定义 TaskQueue 配合拒绝策略"] THREAD --> T2["Netty: NioEventLoopGroup\nSingleThreadEventExecutor 模型"] THREAD --> T3["Dubbo: 多种线程池策略\nFixed/Cached/Limited/Eager"] THREAD --> T4["RocketMQ: Broker 线程池组\nSendMessage/PullMessage 等"] ROOT --> MAP["ConcurrentHashMap"] MAP --> M1["Spring IOC: singletonObjects\n所有单例 Bean 的存储容器"] MAP --> M2["Netty: DefaultChannelHandlerContext\nChannel 属性存储"] MAP --> M3["Tomcat: Servlet 映射表\nURL → Servlet 的路由缓存"] ROOT --> QUEUE["BlockingQueue"] QUEUE --> Q1["Logback: AsyncAppender\nArrayBlockingQueue 异步写日志"] QUEUE --> Q2["Tomcat: TaskQueue\n继承 LinkedBlockingQueue"] QUEUE --> Q3["Disruptor: RingBuffer\n虽非JUC但思想同源"] ROOT --> LIST["CopyOnWriteArrayList"] LIST --> L1["Tomcat: Session 监听器列表\n遍历时无需加锁"] LIST --> L2["Spring: ApplicationListener 集合\n事件多播时安全迭代"] class ROOT root; class THREAD,MAP,QUEUE,LIST branch; class T1,T2,T3,T4,M1,M2,M3,Q1,Q2,Q3,L1,L2 leaf; class T1,T2,M1,Q1,L1 highlight; 🏊 线程池在中间件中的应用 🏊 Tomcat:请求处理的线程池引擎 Tomcat 处理 HTTP 请求的核心是一个定制化的 ThreadPoolExecutor 。它没有直接用 JDK 的标准实现,而是继承了 ThreadPoolExecutor 并重写了其中的关键行为。 ...

十月 2, 2022 · 9 分钟 · 1834 字 · yaomingye

MQTT 协议

📡 MQTT 协议:角色体系、Broker 原理与 QoS 分级机制全解析 问题切入:一个智能家居的消息困境 假设你要开发一个智能家居系统,包含以下设备: 10 个温湿度传感器,每 5 秒上报一次数据 5 个智能插座,需要接收开关指令并上报当前功率 1 个手机 App,需要实时看到所有设备的状态,并能下发控制指令 你的第一反应可能是用 HTTP:传感器 POST 数据到服务端,App 轮询拉取最新状态。但很快问题就来了: 传感器数量 × 上报频率 = 10 × (1 / 5s) = 2 QPS 的上报请求 App 轮询最新状态 = 1 × (1 / 2s) = 0.5 QPS 的查询请求 设备控制指令 = App POST 到服务端,服务端再推给设备... HTTP 是请求-响应模式,服务端无法主动向设备推送指令。如果让设备轮询指令,延迟高且浪费带宽。而且温湿度传感器是低功耗设备(电池供电的 ESP8266),HTTP 的 TCP 三次握手 + Header 开销太大。 这就是 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)解决的问题:它是一个 发布-订阅模式 的轻量级消息协议,专为低带宽、高延迟、不可靠网络下的物联网设备通信而设计。 MQTT 的角色体系 MQTT 协议定义了三种角色。大部分文章对它们的介绍含糊其词,这里逐个讲清楚。 ...

十月 1, 2022 · 14 分钟 · 2882 字 · yaomingye

云服务器选型实战

☁️ 云服务器选型实战:带宽、CPU、内存容量估算方法论 —— 以阿里云 ECS 为例 📌 一、问题切入:新项目上云,ECS 实例怎么选? 小张接到一个新项目——做一个面向 C 端用户的电商小程序后端,预计日均 UV 5 万,高峰期 QPS(每秒请求数)约 500 📊。技术栈是 Spring Boot + MySQL + Redis,全部部署在阿里云 ECS 上。 他打开阿里云 ECS 购买页面,面对几十种实例规格、上百个配置组合: ecs.g7.large 2vCPU 8GB 最高 10Gbps ecs.c7.xlarge 4vCPU 8GB 最高 12.5Gbps ecs.r7.large 2vCPU 16GB 最高 10Gbps ecs.g7.xlarge 4vCPU 16GB 最高 12.5Gbps ... 选低了——大促时服务崩掉 💥,用户投诉;选高了——老板看账单时脸色不好 😤。 服务器选型的本质是对 三个核心维度 的估算: 带宽(网络吞吐) 、 CPU(计算能力) 、 内存(数据缓存空间) 。三个维度相互独立又彼此制约,高估任何一个都是浪费 💸,低估任何一个都是事故 🚨。本文将给出每个维度的 可量化估算公式 ,结合阿里云 ECS 的具体实例规格,形成一套可复用的选型标准。 🔍 二、估算前置:三个维度的关系 选型之前,先明确三个维度分别决定什么: ...

九月 29, 2022 · 10 分钟 · 1926 字 · yaomingye

API 响应封装

API 响应封装:统一返回格式、全局自动包装与异常处理全解析 🤔 1. 问题切入:一个没有封装的 Controller 是怎样的? 在开始讲解之前,先看一段没有做任何统一封装的 Controller 代码: @RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/findById") public ProductEntity findById(Long id) { ProductEntity product = productService.findById(id); if (product == null) { // 直接返回 null,前端收到空响应体,不知道发生了什么 return null; } return product; } @PostMapping("/insert") public String insert(@RequestBody ProductEntity product) { try { productService.insert(product); return "success"; // 字符串硬编码,前后端契约不统一 } catch (Exception e) { return e.getMessage(); // 把异常栈暴露给前端,安全风险 } } } 这段代码暴露了三个问题: ...

九月 26, 2022 · 10 分钟 · 2010 字 · yaomingye

Long类型ID前端精度丢失

Long类型ID前端精度丢失:从IEEE 754根因到Jackson全局序列化方案 🤔 一、问题切入:一个"找不着"的订单 某天业务反馈:用户在订单详情页点进去一片空白,后台日志里看到查的是 ID 1857353925587607500,但数据库里根本没有这条记录。翻看上游接口的原始响应体,后端明明返回的是 1857353925587607552。 差了多少?不多,就差了 52:...552 变成了 ...500。但这 52 的差距足以让一条订单从数据库里彻底"消失"。 写个最简单的演示: // 后端:Java Long 值 long orderId = 1857353925587607552L; System.out.println(orderId); // 输出: 1857353925587607552 ✓ 后端没问题。再看前端: // 前端:直接解析后端返回的 JSON const json = '{"orderId": 1857353925587607552}'; const obj = JSON.parse(json); console.log(obj.orderId); // 输出: 1857353925587607500 ✗ 同一个数字,跨了一道 HTTP 就被"阉割"了最后两位精度。这不是哪家框架的 bug,也不是谁写错了代码——根因在 JavaScript Number 的底层存储格式。 flowchart TD classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; JAVA[Java Long\n1857353925587607552] JSON[JSON 数字\n1857353925587607552] PARSE[JavaScript JSON.parse] NUM[JS Number\n1857353925587607500] QUERY[用错误ID查数据库] MISS[查不到数据] JAVA -->|Jackson序列化| JSON JSON -->|HTTP响应| PARSE PARSE -->|IEEE 754精度丢失| NUM NUM --> QUERY QUERY --> MISS class JAVA,JSON data; class NUM,MISS reject; class PARSE,QUERY process; 这个问题的触发条件很具体:后端 Long 值超过 9007199254740991(即 2^53 ~ 1,约 16 位十进制数)时,前端 JSON.parse() 解析出的数字就会丢失精度。雪花算法生成的 ID 通常 17 ~ 19 位,正好踩在坑里。 ...

九月 25, 2022 · 5 分钟 · 956 字 · yaomingye

Spring MVC 常用注解

Spring MVC 常用注解:企业级全场景用法与实战指南 🤔 1. 问题切入:一个订单查询接口 假设你在开发一个电商系统的订单查询接口,需要实现以下需求: 通过订单 ID 查询订单详情 支持按状态、时间范围过滤订单列表 接收 JSON 请求体来创建订单 处理参数校验失败时的错误返回 统一处理各类异常 以下是一个典型的 Spring MVC Controller 初版实现: @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping("/{id}") public Result<Order> getOrder(@PathVariable Long id) { // 查询订单 } @GetMapping public Result<Page<Order>> listOrders( @RequestParam(required = false) String status, @RequestParam(required = false) @DateTimeFormat(iso = DATE) LocalDate startDate, @RequestParam(required = false) @DateTimeFormat(iso = DATE) LocalDate endDate, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { // 分页查询 } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Result<Order> createOrder(@Validated @RequestBody CreateOrderRequest request) { // 创建订单 } } 短短几行代码用到了 10+ 个注解。这些注解各自承担什么职责?组合使用时有什么坑?在企业级项目中应该如何规范使用?这篇博客将系统性地回答这些问题。 ...

九月 24, 2022 · 12 分钟 · 2520 字 · yaomingye

Spring Boot企业开发高频注解完全指南

Spring Boot企业开发高频注解完全指南:从IoC容器到数据访问全覆盖 🤔 一、为什么需要这份注解清单 初学 Spring Boot 时,打开官方文档会看到上百个注解。但实际企业开发中,真正高频使用的注解只有其中一部分。很多注解你可能工作三五年也用不到一次。 本文筛选出企业开发中使用频率最高的 Spring 注解(不含 SpringMVC 和 SpringSecurity),每个注解都配有可运行的示例代码和一句话说明它的用途。不解释底层原理,只告诉你"这是什么、怎么用、什么时候用"。 注解来源范围:Spring Framework + Spring Boot + Spring Data JPA + Spring AOP + Spring Cache + Spring Scheduling + Spring Retry。 约定:下文所有示例均基于 Spring Boot 项目,包路径省略。示例中 @Service、@Repository 等注解未重复展示之处,默认已配合 @ComponentScan 自动扫描。 🗺️ 二、注解分类全景图 在实际进入每个注解之前,先用一张分类图建立全局认知: flowchart LR classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold; classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; ROOT[Spring注解体系] ROOT --> B1[1.IoC容器核心] ROOT --> B2[2.Boot启动配置] ROOT --> B3[3.AOP切面] ROOT --> B4[4.事务管理] ROOT --> B5[5.异步与定时] ROOT --> B6[6.缓存管理] ROOT --> B7[7.数据校验] ROOT --> B8[8.JPA数据访问] ROOT --> B9[9.事件监听] ROOT --> B10[10.测试支持] ROOT --> B11[11.条件装配] ROOT --> B12[12.重试机制] class ROOT root; class B1,B2,B3,B4,B5,B6,B7,B8,B9,B10,B11,B12 branch; 📦 三、Spring IoC 容器核心注解 IoC(控制反转)和 DI(依赖注入)是 Spring 的根基。以下是日常开发中必用的注解。 ...

九月 17, 2022 · 17 分钟 · 3415 字 · yaomingye

Linux IO 模型

Linux IO 模型:阻塞、非阻塞、多路复用与异步 IO 全解析 1 ⚡ 问题切入:一个后端开发者必须回答的问题 假设你在面试中被问到: “一台 4 核 8GB 的服务器,为什么能支撑 10 万个并发连接?” 答案的关键不在于 CPU 有多快、内存有多大,而在于 IO 模型 。如果每个连接用一个线程、每个线程做阻塞 IO,10 万连接就需要 10 万个线程——每个线程消耗约 1MB 栈空间,仅线程栈就占 100GB 内存,4 核 CPU 也根本无法调度这么多线程。 真正让高并发成为可能的,是 非阻塞 IO 和 IO 多路复用 (I/O Multiplexing,单个线程同时监听多个 IO 事件)。Nginx、Redis、Netty 的高性能都建立在正确的 IO 模型选择之上。 这篇博客从操作系统层面讲解 Linux 五大 IO 模型,聚焦于"数据如何从网卡/磁盘到达你的程序",为后续理解 Java NIO、Netty、Kafka 等框架打下理论基础。 2 💻 硬件架构:一次 IO 操作经历了什么 在讨论 IO 模型之前,必须先理解一次 IO 操作涉及哪些硬件组件以及数据如何流转。 如上图所示,一次典型的网络 IO 读取,数据经过以下路径: ...

九月 13, 2022 · 9 分钟 · 1736 字 · yaomingye

JUC 源码阅读路线图

JUC 源码阅读路线图:从 LockSupport 到 ForkJoinPool 的完整导读 ❓ 1️⃣ 一、道格·李的 JUC 有明确的分层设计——读源码必须按这个顺序 道格·李在设计 java.util.concurrent 包时,不是把二十几个类平铺在一个包里的。JUC 有严格的分层:底层原语(CAS、volatile、LockSupport)→ 核心框架(AQS)→ 具体实现(锁、同步器、集合、线程池)。 这个分层意味着:如果你一上来就读 ReentrantLock.lock() 的源码,三行之后就会遇到 tryAcquire(),再往下就是 CAS 修改 AQS state、LockSupport.park() 阻塞线程——全是底层 API。不知道 CAS 的语义,不知道 state 的 CLH 入队流程,不知道 park/unpark 的 permit 机制,每走一步都得暂停查资料,阅读体验极差。 反之,如果你按道格·李的设计顺序来读——先理解 CAS 和 LockSupport(地基),再啃透 AQS(骨架),然后逐一看 ReentrantLock、Semaphore、CountDownLatch 怎么在骨架上加肉(定制 tryAcquire / tryRelease)——整个 JUC 包的结构就一目了然了。 这篇博客提供的就是这份按设计分层排列的源码阅读路线图——告诉你每个组件在 JDK 中的位置、入口 API、核心函数调用链、推荐阅读顺序、以及读完这个组件你能学到什么设计思想。不会逐行分析源码(各组件详细分析见本系列其他文章)。 🏗️ 2️⃣ 二、总览:JUC 全景架构图 阅读之前,先建立全局坐标系。以下是所有 JUC 核心组件的逻辑关系图: flowchart TD classDef base fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold; classDef core fill:#1e1b4b,stroke:#4f46e5,stroke-width:2px,color:#e0e7ff,font-weight:bold; classDef lock fill:#1e1b4b,stroke:#4f46e5,stroke-width:2px,color:#e0e7ff,font-weight:bold; classDef sync fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold; classDef coll fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef pool fill:#0f172a,stroke:#3b82f6,stroke-width:1.5px,color:#bfdbfe,font-weight:bold; classDef tl fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; BASE[🔧 底层原语] BASE --> CAS[CAS\nUnsafe.compareAndSwapX] BASE --> VOL[volatile\n内存可见性] BASE --> PARK[LockSupport\npark/unpark] PARK --> AQS[AbstractQueuedSynchronizer\nAQS框架] CAS --> AQS VOL --> AQS AQS --> RL[ReentrantLock] AQS --> RW[ReentrantReadWriteLock] AQS --> SEM[Semaphore] AQS --> CDL[CountDownLatch] AQS --> FUT[FutureTask] AQS --> TPE[ThreadPoolExecutor] PARK --> CB[CyclicBarrier] CAS --> CHM[ConcurrentHashMap] CAS --> CLQ[ConcurrentLinkedQueue] VOL --> CHM RL --> COW[CopyOnWriteArrayList] AQS -.-> BQ[BlockingQueue] TPE --> STPE[ScheduledThreadPoolExecutor] TPE -.-> FJP[ForkJoinPool] CB --> TL[ThreadLocal] TL --> ITL[InheritableThreadLocal] ITL --> TTL_CLASS[TransmittableThreadLocal] class BASE base; class AQS core; class RL,RW lock; class SEM,CDL,CB,FUT sync; class CHM,CLQ,COW,BQ coll; class TPE,STPE,FJP pool; class TL,ITL,TTL_CLASS tl; 这张图揭示了 JUC 的设计分层: ...

九月 6, 2022 · 12 分钟 · 2397 字 · yaomingye

ThreadLocal 线程池上下文传递

ThreadLocal 线程池上下文传递:从 InheritableThreadLocal 缺陷到 TransmittableThreadLocal 全解析 🤔 一、JDK 设计者为什么要给每个线程配一个"私房钱罐" 多线程编程中有一个经典矛盾:线程之间要共享一部分数据来协作,又要有各自私有的数据来隔离。共享数据靠锁来保护,私有数据呢?如果每建一个新线程都要手动传参数、写包装类,代码很快就变成意大利面条。 JDK 1.2 的设计者(Josh Bloch 等人)给出的方案是 ThreadLocal——每个线程维护一个私有的 ThreadLocalMap,key 是 ThreadLocal 实例,value 是你想隔离的数据。同一个 ThreadLocal 对象在不同线程中的值互不干扰。这个设计让"线程级上下文"(traceId、事务、用户 Session)变得自然:只要在主线程 set 一下,当前线程的任何方法都能 get 到,不需要在方法签名里一路传参。 但 JDK 设计者很快发现一个新问题:ThreadLocal 在线程间是完全隔离的——如果父线程 set 了值,新建子线程时,子线程拿不到。这就是为什么后来又有了 InheritableThreadLocal:它在 Thread 构造函数中触发 init(),将父线程 ThreadLocalMap 中标记为可继承的条目浅拷贝到子线程。 然而,ITL 的设计有一个致命缺陷——它只在 new Thread() 时触发传递。线程池复用已有线程,不再走 Thread 构造函数,ITL 的传递逻辑完全不执行。第一次提交任务时碰巧用的是刚创建的新线程(触发了一次传递),第二次复用同一个线程时,父线程的新值就传不过来了。 这就是阿里开源的 TransmittableThreadLocal 要解决的问题。它的核心思路是:不再依赖线程创建时的一次性传递,而是在每次提交任务时主动 capture 父线程的快照 → replay 到工作线程 → 任务完成后再 restore 还原。 阅读本篇文章的收获: InheritableThreadLocal 是如何在 Thread 构造函数中实现传递的?源码在哪一行触发? 为什么它在线程池中会失效?根源在 JDK 源码的哪一行? TransmittableThreadLocal 又是如何在源码层面解决这些缺陷的? capture() / replay() / restore() 三个方法各自做了什么? 🧵 二、ThreadLocal 基础回顾:数据到底存在哪里 在深入 ITL 和 TTL 之前,先快速回顾 ThreadLocal 的核心数据结构。如果你已经熟悉这部分,可以直接跳到第三章。 ...

九月 5, 2022 · 13 分钟 · 2586 字 · yaomingye
Cat Radio