AQS 源码重走:从 Doug Lea 的 CLH 变体到 JDK 21 的完整路径

AQS 源码重走:从「如果让我写」到「原来还可以这样」 某开发者背了一周 AQS 八股:CLH 队列、acquireQueued、shouldParkAfterFailedAcquire……面试官问「AQS 的 prev 和 next 为什么一个可靠一个不可靠?」——答了,但说不出这么设计是为了解决什么问题。面试官一句话戳穿:「你读过源码,但你有没有想过在并发场景下删队列中的一个节点,不这么写会发生什么?」 背源码最大的问题是:你不知道那些代码是在解决什么问题。 所以这篇文章不走寻常路——先问「如果不用 AQS,让我自己写一个锁排队框架,该怎么下手?」然后一步步推演,当你发现「哎这里不安全怎么办」的时候,再引出 Doug Lea 的解法。这个过程会让人惊叹:原来并发代码还可以这样写。 最后,会专门对比 JDK 8 和 JDK 21 两个版本的 AQS,讲清楚 Doug Lea 为什么在 JDK 21 中对核心逻辑做了一次大重构。 🏗️ 从零开始:如果让我实现一个可排队的锁 假设只有 synchronized 和 LockSupport.park/unpark ,让你实现一个「锁没抢到就排队等」的框架,你打算怎么写? 第一次尝试:一个粗暴的实现 // 初版思路 class NaiveLock { volatile int state = 0; // 0=未锁, 1=已锁 Queue<Thread> queue = new ... // 等待队列 void lock() { while (!CAS(&state, 0, 1)) { // 没抢到锁 queue.enqueue(currentThread()); // 入队 park(); // 阻塞 } } void unlock() { state = 0; // 释放锁 Thread t = queue.dequeue(); // 从队列取一个 unpark(t); // 唤醒 } } 看上去好像没问题。但仔细想想,全是坑: ...

十月 21, 2023 · 20 分钟 · 4200 字 · yaomingye

JVM类加载与反射:从Class文件到运行时类的全景——Klass模型、类加载器、Metaspace与反射的桥接机制

反射凭什么认识你的类 某开发者写过这样一段代码,当时觉得平平无奇: Method method = obj.getClass().getDeclaredMethod("secretLogic", String.class); method.setAccessible(true); Object result = method.invoke(obj, "hacked!"); 运行完才反应过来——凭什么?一个 Class 对象拿在手里,连 private 方法都能翻出来调用,连参数签名都一清二楚。JVM 到底在背后存了什么东西,让反射可以"看见"一个类的全部内脏? 答案就在 HotSpot 的 Klass 模型 里。 📌 前置知识:本文假设读者知道 .class 文件是 javac 编译产物、JVM 基本内存分区(堆、栈、方法区)的常识。如果不清楚方法区和 Metaspace 的关系,后文有图。 全景先览:JVM 中的类在哪 在深入 Klass 之前,先看一张全局地图——一个类从 .class 文件到进入 JVM 运行时,到底经过了哪些区域。 这张图先有一个印象就行——关键记住一点:一个类被加载后,JVM 在 Metaspace 里存了一份"类模板"(InstanceKlass),在 Heap 里放了一个轻量的"Java 镜像"(Class 对象)。 反射读的元信息来自前者,你代码里 getClass() 拿到的是后者。 类模板:HotSpot 眼中的"类" 你写的每一个 Java 类,在 HotSpot 内部都有一个对应的 C++ 对象来描述它——这就是 Klass 模型。 Klass 继承链 Klass ← 所有类的抽象基类 ├── InstanceKlass ← 普通 Java 类(你写的 99% 的类) │ ├── InstanceMirrorKlass ← Class 对象本身(镜子) │ └── InstanceRefKlass ← 引用类型(软/弱/虚引用) ├── ArrayKlass ← 数组类 │ ├── TypeArrayKlass ← 基本类型数组(int[]) │ └── ObjArrayKlass ← 对象数组(String[]) ⚠️ 新手提示:Klass 不是 Class。Klass 是 C++ 层面的数据结构,存在 Metaspace; java.lang.Class 是 Java 层面的对象,存在 Heap。日常说"类模板"通常指 InstanceKlass。 ...

二月 22, 2023 · 3 分钟 · 590 字 · yaomingye

JVM 面试突击

🔬 JVM 面试突击:运行时数据区、类加载、GC 与调优全解析 📌 前置知识:阅读本文需要具备 Java 基础语法知识、对 JVM 有初步概念(知道 JVM 是运行 Java 程序的虚拟机即可)。本文定位为面试突击速查手册,每个考点都按"面试怎么答"组织,命令部分附带完整的操作步骤和输出解读。 各模块面试频率参考 在开始具体考点之前,先了解各模块的面试出现频率,有助于合理分配复习时间: 模块 面试频率 关键程度 运行时数据区 ⭐⭐⭐⭐⭐ 每场必问,入门级考点 类加载机制 ⭐⭐⭐⭐⭐ 双亲委派模型高频出现 垃圾回收机制 ⭐⭐⭐⭐⭐ 区分候选人水平的关键 调优工具与实战 ⭐⭐⭐⭐ 考察实际动手能力 JMM + volatile ⭐⭐⭐⭐⭐ 并发底层原理 经典面试题 ⭐⭐⭐⭐ 综合应用能力 📌 一、JVM 运行时数据区:内存布局与职责 🧠 1.1 JVM 内存布局全景图 这是面试最常考的入门题,必须清楚每个区域的功能、是否为线程共享,以及各自可能抛出的异常。下面先用 Mermaid 展示 JVM 内存区域的整体分类: flowchart LR JVM(["🔷 JVM 运行时数据区"]) JVM --> SHARED["👥 线程共享区"] JVM --> PRIVATE["🔒 线程私有区"] SHARED --> HEAP["📦 Java堆\n对象实例 / 数组\nGC 主要区域"] SHARED --> METHOD["📋 方法区\n类信息 / 常量 / 静态变量\nJDK8+ 元空间实现"] PRIVATE --> PC["📍 程序计数器\n字节码行号指示器\n无OOM"] PRIVATE --> VMSTACK["📚 虚拟机栈\n栈帧: 局部变量表+操作数栈+动态链接\nStackOverflowError / OOM"] PRIVATE --> NATIVE["🔧 本地方法栈\nnative 方法服务\nStackOverflowError / OOM"] 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 private fill:#1e293b,stroke:#0284c7,stroke-width:1.5px,color:#f8fafc; class JVM root class SHARED,PRIVATE branch class HEAP,METHOD leaf class PC,VMSTACK,NATIVE private 下面用 HTML+CSS 布局图精确展示 JVM 内存各区域的相对位置、大小关系和内部结构: ...

十月 18, 2022 · 12 分钟 · 2372 字 · yaomingye

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

Netty

Netty:Reactor 线程模型、Pipeline 责任链与四个必写示例全解析 1 ⚡ 问题切入:原生 NIO 能工作,但你敢上生产吗? 在上一篇 Java NIO 博客中,我们手写了一个 NIO EchoServer——单线程管理多个连接,Selector 封装 epoll。这段代码在演示环境中运行良好,但如果直接部署到生产环境,会遇到四个棘手问题: // 原生 NIO EchoServer 的核心循环(看似正确,实则隐患重重) while (true) { selector.select(); for (SelectionKey key : selector.selectedKeys()) { if (key.isAcceptable()) { SocketChannel client = ssc.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); int len = client.read(buf); // 问题1: 读到半包怎么办? buf.flip(); // 问题2: 忘了 flip 直接炸 client.write(buf); // 问题3: 写不出去谁管? buf.clear(); // 问题4: clear 还是 compact? } keyIterator.remove(); } } 原生 NIO 的四大生产痛点: ...

九月 15, 2022 · 13 分钟 · 2657 字 · yaomingye

Java NIO

Java NIO:epoll 多路复用、Buffer 机制与单线程高并发全解析 1 ⚡ 问题切入:一个线程如何管理 10000 个连接? 在经典的 BIO (Blocking I/O,阻塞 I/O)模型下,每个 Socket 连接需要分配一个独立线程。当 accept() 返回一个新连接,就启动一个线程去 read() —— 这个线程在数据到达之前会一直阻塞,CPU 时间被白白浪费在线程上下文切换上。 // 传统 BIO:一个连接一个线程(不可行) ServerSocket server = new ServerSocket(8080); while (true) { Socket client = server.accept(); // 阻塞等待连接 new Thread(() -> { InputStream in = client.getInputStream(); byte[] buf = new byte[1024]; in.read(buf); // 阻塞等待数据 // 处理数据... }).start(); } 问题:如果有 10,000 个连接,就需要 10,000 个线程。每个 Java 线程默认栈大小约 1MB,仅线程栈就消耗 10GB 内存,而且 CPU 绝大多数时间都在做线程切换而非真正处理数据。这就是著名的 C10K 问题 (Client 10,000 Problem)。 ...

九月 14, 2022 · 17 分钟 · 3496 字 · yaomingye

RandomAccessFile 文件指针与任意位置读写

RandomAccessFile 文件指针与任意位置读写:多线程分段下载实现 1 ⚡ 问题切入:流式读写的局限 前面介绍的 FileInputStream / FileOutputStream 都是 顺序流 (Sequential Stream),只能从文件头开始,一个字节接一个字节地读写。如果只想读文件的第 1000 个字节,顺序流必须先把前面 999 个字节读完(或调用 skip()),效率很低。 如果要实现以下场景,顺序流就完全不够用了: 修改一个 1GB 文件的中间某几个字节 在文件末尾追加日志,同时读取文件头部的元数据 多线程分段下载文件,每个线程负责不同区间的数据块 这些场景需要 随机读写 ——可以自由移动文件指针到任意位置,像操作数组下标一样操作文件。Java 提供了 RandomAccessFile 来实现这个能力。 // 随机读:直接跳到第 1000 字节开始读 RandomAccessFile raf = new RandomAccessFile("data.bin", "r"); raf.seek(1000); // 指针移到 position=1000 int b = raf.read(); // 读第 1000 个字节 System.out.printf("第1000字节: 0x%02X%n", b); raf.close(); 跳过前面 999 个字节,直达目标位置——这就是 RandomAccessFile 的价值。 2 🎯 核心概念:文件指针 2.1 ❓ 什么是文件指针 文件指针 (File Pointer)是 RandomAccessFile 内部维护的一个 long 类型偏移量(Offset),表示 下一次读写操作将发生的字节位置 。它类似于数组的下标索引,但作用对象是文件。 ...

九月 12, 2022 · 8 分钟 · 1611 字 · yaomingye

Java IO 缓冲流与装饰器模式

Java IO 缓冲流与装饰器模式:从性能对比到设计模式全解析 1 ⚡ 问题切入:为什么单字节读取一个 10MB 文件要 30 秒? 先看一段能跑的代码。下面这段程序用 FileInputStream 的 read() 方法,一个字节一个字节地读取一个 10MB 的文件,并写入到另一个文件: // 无缓冲:单字节读取 try (FileInputStream fis = new FileInputStream("source_10mb.bin"); FileOutputStream fos = new FileOutputStream("dest_10mb.bin")) { int b; while ((b = fis.read()) != -1) { // 每次只读 1 字节 fos.write(b); // 每次只写 1 字节 } } 在我的机器上(Windows 11,SSD),这段代码的运行耗时约为 28,000 ms (28 秒)。 现在再看另一段代码,功能完全一样,只是在外层各包了一个缓冲流: // 有缓冲:仍然单字节读取 try (BufferedInputStream bis = new BufferedInputStream( new FileInputStream("source_10mb.bin")); BufferedOutputStream bos = new BufferedOutputStream( new FileOutputStream("dest_10mb.bin"))) { int b; while ((b = bis.read()) != -1) { // 仍然每次只读 1 字节 bos.write(b); // 仍然每次只写 1 字节 } } 耗时:约 150 ms 。两者相差约 180 倍 。代码逻辑完全一样(都是循环内单字节读写),一行包的差别,性能天差地别。 ...

九月 9, 2022 · 10 分钟 · 1942 字 · yaomingye

Java IO API 基础入门

Java IO API 基础入门:File 类、字节流与字符流使用指南 1 📁 File 类(文件路径操作) java.io.File 是 Java IO 包中最基础的类,它表示文件系统中的一个 路径 (文件或目录),提供创建、删除、判断、遍历等操作。核心概念:File 对象只是一个 路径的抽象表示 ,创建 File 对象时不会检查文件或目录在磁盘上是否真实存在。 1.1 🏗️ 构造器与路径表示 构造器 说明 new File("path") 接收一个路径字符串,支持相对路径和绝对路径 new File("parent", "child") 接收父路径和子路径,自动拼接分隔符 File f1 = new File("D:/data/test.txt"); // 绝对路径 File f2 = new File("./data/test.txt"); // 相对路径(相对于项目根目录) File f3 = new File("D:/data", "test.txt"); // 父路径 + 子路径 关键陷阱 :new File("path") 只是在内存中构造了一个路径对象,不会检查文件是否真实存在 ,也不会创建文件。这意味着即使路径指向一个不存在的文件,构造器也不会抛异常。 1.2 🔍 文件/目录判断 判断一个路径在磁盘上是否存在、是文件还是目录: 方法 返回值 说明 exists() boolean 路径对应的文件或目录是否存在 isFile() boolean 是否存在且是文件(不是目录) isDirectory() boolean 是否存在且是目录(不是文件) File file = new File("D:/data/test.txt"); if (file.exists()) { System.out.println(file.isFile() ? "是文件" : "是目录"); } else { System.out.println("路径不存在"); } 注意事项: ...

九月 7, 2022 · 5 分钟 · 1024 字 · yaomingye

Phaser 可重用动态线程同步屏障

Phaser 可重用动态线程同步屏障:双栈编排机制、64 位状态字与多阶段调度全解析 🤔 一、道格·李为什么需要比 CyclicBarrier 更灵活的屏障 CountDownLatch 和 CyclicBarrier 分别覆盖了两种同步场景:前者是"一个线程等 N 个线程完成",后者是"N 个线程彼此等到齐后一起走"。但道格·李在后续实践中发现了一个覆盖盲区:多阶段计算中,每阶段的参与者数量可能并不相同。 举个例子:分片计算一个大型数据集,第一阶段 8 个线程并行处理各自的分片;第二阶段某些分片的数据已经为空,对应的线程应该退出,剩下 5 个线程继续;第三阶段可能又加入 2 个新线程处理汇总结果。 CountDownLatch 做不到——它是一次性的,三个阶段需要三个实例。 CyclicBarrier 也做不到——它的 parties 数量在构造时固定,运行期间不能增删参与者。如果有线程中途退出,CyclicBarrier 会永远等不到第 N 个线程而永久阻塞(或者触发 BrokenBarrierException)。 道格·李因此在 Java 7 引入了 Phaser:一个支持动态参与者数量 + 多阶段循环使用的同步屏障。线程可以在运行时通过 register() 加入、通过 arriveAndDeregister() 退出,Phaser 自动调整每轮的等待计数。内部用一个 64 位的 state 字段打包了阶段号、已到达计数、未到达计数等所有状态信息,通过 CAS 无锁操作更新——这是 JUC 中最复杂的一个状态字设计。 🎚️ 二、Phaser 核心概念(术语定义) 在深入源码前,先明确几个关键术语: 术语 定义 类比理解 Phase(阶段号) 从 0 开始递增的整数,每轮同步完成后 +1 表示"第几轮同步" Party(参与者) 注册到 Phaser 中的一个线程/任务 需要等待的对象 Unarrived(未到达数) 当前阶段尚未调用 arrive() 的参与者数量 每到达一个就减 1 Arrive(到达) 线程调用 arrive() 表示完成当前阶段工作 通知 Phaser"我到了" Advance(推进) 当 unarrived 归零时,phase 自增,进入下一轮 所有人都到了,开始下一阶段 Register(注册) 增加一个参与者(parties + 1, unarrived + 1) 动态加入 Deregister(注销) 减少一个参与者(parties - 1, unarrived - 1) 动态退出 Termination(终止) Phaser 进入终止态,所有操作立即返回负数 强制结束,不再同步 🏗️ 三、数据结构展开 🔢 3.1 64 位状态字:所有信息的原子载体 Phaser 没有使用 AQS,而是直接将全部状态压缩在一个 AtomicLong(字段名 state)中。这是理解 Phaser 的根基。 ...

九月 7, 2022 · 11 分钟 · 2232 字 · yaomingye
Cat Radio