Java IO 进阶

Java IO 进阶:基本类型 IO、打印流与对象序列化实用指南 1 🎮 问题场景:从保存游戏分数说起 假设你正在开发一个本地小游戏,需要把玩家的最高分(int)、胜率(double)和昵称(String)保存到文件,下次启动时读回来。用前面学过的 FileWriter 写文本当然可以,但你需要手动处理类型转换——写的时候 int → String,读的时候 String → int,格式稍微不一致就解析失败。 有没有办法直接把 int 按固定 4 字节的二进制格式写入,读的时候也按 int 原样读出?这就是 基本类型 IO 要解决的问题。 更进一步,如果整个游戏状态是一个复杂的 Java 对象(玩家信息、关卡进度、道具列表),能不能 一键保存整个对象,一键读回 ?这就是 对象序列化 要解决的问题。 本篇覆盖 Java IO 的第四个进阶阶段,依次讲解三种机制: 机制 用途 一句话描述 DataInputStream / DataOutputStream 读写基本类型和字符串 按固定字节数的二进制格式读写,必须按序操作 PrintStream / PrintWriter 格式化文本输出 不抛异常,日常 System.out.println() 就在用它 ObjectInputStream / ObjectOutputStream 对象序列化与反序列化 一键将整个对象转为字节流保存或传输 2 💾 基本类型 IO:DataOutputStream / DataInputStream 2.1 ❓ 是什么 DataOutputStream 和 DataInputStream 是 装饰器流 (包装已有的 OutputStream / InputStream),提供直接读写 Java 基本类型和 String 的能力。数据以 平台无关的二进制格式 写入——比如 writeInt(42) 始终写 4 个字节,无论在什么操作系统上,读出来的值都不变。 ...

九月 9, 2022 · 5 分钟 · 937 字 · yaomingye

Java IO 编码与桥接

Java IO 编码与桥接:字符集、编码转换与乱码解决方案全解析 1 ⚠️ 问题切入:一段乱码代码 先看一段在实际开发中经常遇到的代码。这段代码在不同操作系统上运行,结果 完全不同 : public class GarbledDemo { public static void main(String[] args) throws Exception { // 在 Windows 中文系统上运行(默认 GBK) try (FileWriter writer = new FileWriter("hello.txt")) { writer.write("你好,世界!"); } // 在 Linux 服务器上读取(默认 UTF-8) try (FileReader reader = new FileReader("hello.txt")) { char[] buf = new char[1024]; int len = reader.read(buf); System.out.println(new String(buf, 0, len)); // 输出:你好,世界! ← 正常 // 还是:���← 乱码? // 取决于操作系统! } } } 为什么同一段代码在不同环境下表现不同?因为 FileReader / FileWriter 使用 JVM 默认编码 (通常是操作系统默认编码),而 Windows 中文版默认是 GBK ,Linux 默认是 UTF-8 。写入和读取时编码不一致,就会产生 乱码 (Mojibake,指因字符编码不匹配导致的不可读字符)。 ...

九月 9, 2022 · 8 分钟 · 1535 字 · 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

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

CyclicBarrier 可循环屏障

CyclicBarrier 可循环屏障:源码解析、代际机制与 CountDownLatch 对比全解析 🤔 一、道格·李为什么需要一个可循环的屏障 CountDownLatch 解决了一个问题:一个线程等待多个线程完成操作。但道格·李在设计 JSR 166 时意识到,还有一种更复杂的同步场景没有覆盖:多个线程彼此等待——所有线程都到达同一个"集合点"后,再一起继续往下走。这在分片并行计算中非常常见:N 个线程各算各的,算完之后需要"对表"(交叉校验、汇总),然后继续算下一阶段。 CountDownLatch 做不了这件事——它是一次性的,计数器归零后无法重置。而且它的语义是"一个线程等 N 个线程",不是"N 个线程彼此等"。 Thread.join() 也做不了——join() 等的是线程终止,不是线程到达某个执行点。如果线程需要继续执行(而不是终止),join() 完全不对路。 道格·李因此设计了 CyclicBarrier:一组线程各自执行到某个"屏障点"后调用 await(),先到的线程阻塞等待,直到最后一个线程也到达屏障,所有线程同时被唤醒,继续往下执行。屏障打开后自动重置,可以用于下一个阶段——这就是 Cyclic(可循环)的含义。 与 CountDownLatch 的核心设计区别: CountDownLatch:外部协调者等待 N 个工人完成任务(一次性,一个等 N 个) CyclicBarrier:N 个工人彼此等到齐后一起行动(可循环,N 个彼此等) 🔄 二、数据结构展开:CyclicBarrier 的六大核心字段 📌 2.1 字段总览 // java.util.concurrent.CyclicBarrier public class CyclicBarrier { private final ReentrantLock lock = new ReentrantLock(); // ① 锁 private final Condition trip = lock.newCondition(); // ② 条件队列 private final int parties; // ③ 参与方总数 private final Runnable barrierCommand; // ④ 屏障动作 private Generation generation = new Generation(); // ⑤ 当前代际 private int count; // ⑥ 倒计数 // 内部类——代际 private static class Generation { Generation() {} // 默认 broken = false boolean broken; // 当前代是否被打破 } } 用一张结构图展示这些字段之间的关系: ...

九月 5, 2022 · 10 分钟 · 1922 字 · 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

ScheduledThreadPoolExecutor 定时调度增强

ScheduledThreadPoolExecutor 定时调度增强:DelayedWorkQueue 二叉堆延时队列与 Spring 体系实战 🚀 道格·李为什么需要一个能定时的线程池 Java 1.3 引入的 java.util.Timer 是 JDK 最早提供的定时任务工具。但它有两个致命设计缺陷:① 单线程执行——一个任务执行时间过长,后面的所有任务都会延迟;② 异常吞没导致线程终止——任务抛了未捕获异常,Timer 线程静默死亡,剩余任务永远不会执行。第二个问题在生产环境尤其危险——线上定时取消超时订单的任务因为一个 NullPointerException 静默停止,几天后才被发现。 道格·李在设计 JSR 166 时,ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的直接扩展。它的设计策略是复用线程池的全部管理能力(线程生命周期、拒绝策略、钩子方法),只替换两个关键组件: 任务队列:用 DelayedWorkQueue(基于二叉堆的延时队列)替换 BlockingQueue,任务按触发时间排序,堆顶是最先到期的任务 任务类型:用 ScheduledFutureTask 替换普通的 FutureTask,增加了周期执行模式(固定频率 vs 固定延迟)和下次触发时间的计算逻辑 核心改进:线程池里有 N 个工作线程,一个任务异常不会影响其他线程和任务。Timer 的单线程弱点不再存在。 🏊 ScheduledThreadPoolExecutor 的整体架构 🔗 继承关系与组件概览 ScheduledThreadPoolExecutor 直接继承 ThreadPoolExecutor,在父类基础上替换了三个关键组件: 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[ScheduledThreadPoolExecutor\n继承 ThreadPoolExecutor] ROOT --> B1(1. 任务类型替换) B1 --> TASK["📦 ScheduledFutureTask\nextends FutureTask\n+ implements Delayed\n+ 三态 period 模型\n+ sequenceNumber 保序"] ROOT --> B2(2. 队列替换) B2 --> QUEUE["📥 DelayedWorkQueue\n自建二叉堆\n无界阻塞延迟队列\n扩展 leader/follower 模式"] ROOT --> B3(3. 调度方法替代) B3 --> SCHED["⚡ 三个入口方法"] SCHED --> S1["schedule()\n一次性延迟任务\nperiod = 0"] SCHED --> S2["scheduleAtFixedRate()\n固定速率\nperiod > 0"] SCHED --> S3["scheduleWithFixedDelay()\n固定延迟\nperiod < 0"] ROOT --> B4(4. 关闭后行为) B4 --> SHUT["🛑 两个布尔开关"] SHUT --> C1["continueExistingPeriodicTasksAfterShutdown\nshutdown 后是否继续执行周期任务"] SHUT --> C2["executeExistingDelayedTasksAfterShutdown\nshutdown 后是否执行已延迟的任务"] ROOT --> B5(5. 线程数策略) B5 --> SIZE["🔢 maximumPoolSize = Integer.MAX_VALUE\n队列无界,永远不需要额外线程\n仅核心线程数决定并发度"] class ROOT root; class B1,B2,B3,B4,B5 branch; class TASK,QUEUE,SCHED,SHUT,SIZE leaf; class S1,S2,S3,C1,C2 highlight; 五个改进点一句话总结 : ...

九月 4, 2022 · 12 分钟 · 2502 字 · yaomingye

CompletableFuture 异步编排

CompletableFuture 异步编排:Completion 链表与多中间件应用全解析 🤔 一、道格·李为什么需要比 Future 更强大的异步工具 Java 5 引入了 Future 接口和 FutureTask 实现,解决了"异步执行、获取返回值"的基础需求。到 Java 7 时代,Future 的局限已经非常明显:它只是一个结果的容器,没有回调机制——你不能在结果就绪时自动触发下一步操作,只能调用 get() 阻塞等待。 这在简单的"提交任务→等待结果"场景中够用,但面对以下需求时完全无力: 链式编排:A 的结果作为 B 的输入,B 完成后触发 C。用 Future 只能嵌套 get(),代码缩进越来越深 多结果组合:等 A、B、C 三个结果全部就绪后做汇总。用 Future 只能逐个 get(),最慢的那个决定了总耗时 异常传播:Future.get() 把异常包装成 ExecutionException,调用方需要捕获后 getCause()——异常处理散落在各处 道格·李在设计 CompletableFuture(Java 8 引入)时参考了 JavaScript 的 Promise 模式和函数式编程中的 monad 概念。核心思路是:把异步计算的结果建模为一条流水线——每个阶段接受上一个阶段的输出,产生下一个阶段的输入,阶段之间通过回调串联。这样开发者不需要手动管理线程和等待,只需要"声明"各个步骤之间的关系: // 声明式:订单+用户拼好,再拼优惠券 orderFuture.thenCombine(userFuture, this::mergeOrderUser) .thenCombine(couponFuture, this::assembleFinal); CompletableFuture 的革新在于把异步编程从"命令式等结果"推到了"声明式编排"——关心的不是什么时候拿到结果,而是结果拿到之后要做什么。 🔮 二、数据结构:CompletableFuture 内部长什么样 ⚙️ 2.1 核心字段 从 JDK 源码中看 CompletableFuture<T> 的结构定义(Java 8,java.util.concurrent.CompletableFuture): ...

九月 3, 2022 · 9 分钟 · 1825 字 · yaomingye
Cat Radio