编辑
2026-08-31
Java基础
00

目录

一、BIO
1.1 Java I/O:从流到阻塞
1.2 字符流、字节流
1.3 代码实战Java I/O的几种使用情况
1.3.1 使用字节读写外部文件
1.3.2 既然字节流已经能读写文件了,为什么还要有字符流?
1.3.3 内存中常用的操作类
1.3.4 线程间通信与IO有何关联
1.3.5 网络Socket
1.4 装饰器模式
二、总结

一、BIO

      BIO = Blocking I/O(同步阻塞 I/O)。当线程发起 I/O 调用后,如果数据未就绪,调用不会返回,线程被操作系统挂起、原地干等 —— 注意是“挂起不占 CPU”,不是“耗时长”。Java 里 BIO 的落地 = java.io 流 + java.net 阻塞式 Socket。典型场景是网络:客户端 socket.getInputStream().read() 在对端不发数据时一直挂着,服务端 accept() 也阻塞等连接 —— 两个阻塞点叠加,导致经典 BIO 服务端只能"一个连接一个线程”,这就是它在高并发下崩掉的根源,也是下一节 NIO 登场的原因。

1.1 Java I/O:从流到阻塞

      在正式进入阻塞式案例学习前,先铺垫一些 IO 基础。IO 指的是程序与外部数据源/目的地之间的数据交换,分为输入流和输出流。参照系是程序(JVM)本身:

  • 输入流:外部 → 程序,数据进来
  • 输出流:程序 → 外部,数据出去

常见的使用案例:

  • 文件操作,常用的类有 FileInputStream/FileOutputStream、FileReader/FileWriter
  • 内存数组、字符串 ByteArray*Stream、StringReader/StringWriter
  • 另一个线程 管道流 Piped**同一 JVM 内的线程间通信
  • 网络 socket.getInputStream() 返回的就是 InputStream Socket 本身在 java.net 包
  • 键盘/控制台 System.in(InputStream)、System.out(PrintStream) Scanner 的默认来源

另外 Socket 并不是 java.io的类,但它暴露的接口就是 InputStream —— 这意味着“从网络读”和“从文件读”在代码层面是同一套 API,缓冲流、装饰器可同用。流的抽象屏蔽了设备差异,这也是 java.io设计里让人感觉惊喜的一笔。当然这只是理论,等我敲完代码再对比下可能感觉会更深。

1.2 字符流、字节流

      字符流、字节流在学习初期让我感觉非常混沌。为什么要有字节流和字符流?谁在前谁在后?我该如何梳理并进行高效的运用?

结果显而易见,字节流在前。JDK 1.0只有字节流,物理世界里所有 I/O 设备 —— 磁盘、网卡、键盘 —— 读写的最小单位都是字节,这就出现了一个问题,读取出来的字节都是010101这种电信号,需要程序员对照着UTF-8或者GBK编码表手动的去翻译,流程包括必须知道编码->手动解码->处理边界,很容易出现乱码。另外程序中处理最多的是文本而不是字节。

于是JDK1.1推出了 Reader/Writer字符流,对于字节流来说,字符流的出现简直是救世主,因为你不需要手动解码了,你用 FileReader指定UTF-8编码调用 reader.read(),系统会自动帮你“翻译”,比如 E4 BD A0这三个字节合在一起就是“你”它不会像字节流那样把三个字节拆开给你。而且字符流有缓冲池,底层虽然还是用字节去硬盘里拿数据但 Reader内部有一个默认8192字符大小的缓冲区。它每次从硬盘读字节时,会尽可能多地读然后偷偷在后台把字节凑成完整的汉字放进缓冲区。当你调用 read()时,它直接从缓冲区把拼好的“你”字给你,绝不给你半截字节

所以得出一个结论,字符流 = 字节流 + 内置编码解码器 + 自动处理多字节字符边界的缓冲区,这也就能明白了为什么字符流的入参都是字节流。其实这样记就简单多了:

任何字符流(Reader)如果想从硬盘读“文字”,它第一步必须依赖一个字节流(InputStream)去硬盘里把“字节”读回来,然后字符流再在内存里把这些字节“翻译”成文字。

new InputStreamReader(InputStream in) —— 把网络传过来的字节翻译成文字 new OutputStreamWriter(OutputStream out) —— 把内存里的文字翻译成字节发出去

如果理解了这个点,那相当于抓住了java io的七寸。

1.3 代码实战Java I/O的几种使用情况

1.3.1 使用字节读写外部文件

      我先用三个例子来验证IO读写外部文件,首先是写入一个大数据量测试数据文件,后面对比加入缓冲之后的读取文件的速度;

java
private static final String filePath = "D:\\claude-code\\javaReview\\data\\file.txt"; private static final String BigDataFilePath = "D:\\claude-code\\javaReview\\data\\bigFile.txt"; private static final int TEST_DATA_SIZE = 2 * 1024 * 1024; // 2MB测试数据 /** * 使用字节流写入测试文件(2MB的文本数据) */ private static void testPrepareTestFile(){ File file = new File(BigDataFilePath); if(file.exists()){ System.out.println("测试文件已存在,跳过生成。"); return; } System.out.println("正在生成 2MB 测试文件..."); try (OutputStream os = new FileOutputStream(file)) { BufferedOutputStream bos = new BufferedOutputStream(os); //套一层缓冲 String line = "这是一段测试文本,用来验证IO缓冲的性能差距。1234567890\n"; byte[] bytes = line.getBytes(StandardCharsets.UTF_8); long written = 0; while (written<TEST_DATA_SIZE){ bos.write(bytes); written += bytes.length; } System.out.println("测试文件生成完成!文件大小: " + file.length() / 1024 / 1024 + "MB"); } catch (IOException e) { throw new RuntimeException(e); } } /** * 方式1 :不加缓冲,每次读取一个字节 * @return 时间差 */ private static long testWithoutBuffer(){ System.out.println("测试1:不加缓冲,每次read()1个字节"); long start = System.currentTimeMillis(); try (InputStream is = new FileInputStream(BigDataFilePath)) { int data; int count = 0; while ((data = is.read())!=-1){ count++; } System.out.println("读取了"+count+"个字节"); long end = System.currentTimeMillis(); long cost = end - start; System.out.println(" → 读取了 " + count + " 个字节"); return cost; } catch (IOException e) { throw new RuntimeException(e); } } /** * 方式2 : 加缓冲,每次还是读取一个字节 * @return */ private static long testWithBuffer(){ System.out.println("测试2:加缓冲,每次read()1个字节"); long start = System.currentTimeMillis(); try (InputStream is = new FileInputStream(BigDataFilePath)) { BufferedInputStream bis = new BufferedInputStream(is); //套上缓冲 int data; long count = 0; while ((data = bis.read())!=-1){ count++; } System.out.println(" → 读取了 " + count + " 个字节"); } catch (IOException e) { throw new RuntimeException(e); } long end = System.currentTimeMillis(); long cost = end - start; System.out.println("耗时: " + cost + " ms"); return cost; } /** * 方式3 :缓冲+批量读取 * @return */ private static long testWithBatchBuffer(){ System.out.println("测试3:缓冲+批量读取,每次read(byte[])批量读取 8192 字节"); long start = System.currentTimeMillis(); try (InputStream is = new FileInputStream(BigDataFilePath)) { BufferedInputStream bis = new BufferedInputStream(is); //套上缓冲 byte[] buffer = new byte[8192]; //8kb缓冲区 int bytesRead; long totalBytes = 0; while ((bytesRead= bis.read(buffer))!=-1){ totalBytes +=bytesRead; } System.out.println(" → 读取了 " + totalBytes + " 个字节"); } catch (IOException e) { throw new RuntimeException(e); } long end = System.currentTimeMillis(); long cost = end - start; System.out.println("耗时: " + cost + " ms"); return cost; }

执行结果

powershell
测试文件已存在,跳过生成。 测试1:不加缓冲,每次read()1个字节 读取了2089924个字节 耗时: 5922 ms 测试2:加缓冲,每次read()1个字节 → 读取了 2089924 个字节 耗时: 16 ms 测试3:缓冲+批量读取,每次read(byte[])批量读取 8192 字节 → 读取了 2089924 个字节 耗时: 1 ms

对比测试1和测试2,为什么 BufferedInputStream 能让速度提升 370 倍? 无缓冲逐字节读耗时 5922ms,加一层 BufferedInputStream 后骤降至 16ms。其中的关键在于操作系统与 JVM 之间的交互次数,以及缓冲区域与目标区域(比如数组)的交互次数。

1.为什么 read() 逐字节读这么慢?

每次 FileInputStream.read() 都会触发一次 native 方法调用 → 操作系统 系统调用 → 从磁盘读取 一个字节 → 返回。2MB 文件约 209 万个字节,意味着 209 万次系统调用。系统调用涉及用户态/内核态切换,单次虽微秒级,但 200 万次累加就是秒级耗时。

2.BufferedInputStream 做了什么?

它内部维护了一个 8KB 的字节数组缓冲区。当调用 bis.read() 时: 如果缓冲区有数据,直接从内存返回 —— 0 次系统调用 如果缓冲区空了,才触发一次底层 FileInputStream.read(byte[]),一次性读满 8KB —— 1 次系统调用换回 8192 个字节,209 万次系统调用 → 约 209万 ÷ 8192 = 256,这就是 370 倍差距。

3.那为什么批量 read(byte[]) 更快(1ms)?

即使有了 BufferedInputStream,bis.read() 每次仍返回一个字节,虽然系统调用少了,但 JVM 层面仍有 209 万次方法调用和字节复制。而 bis.read(byte[]) 一次性将 8192 个字节拷贝到目标数组,循环次数从 209 万次降到 256 次,JVM 方法调用开销也被压缩。

1.3.2 既然字节流已经能读写文件了,为什么还要有字符流?

前面提到过字节流在读写的时候会有一些问题比如:

  1. 编码信息丢失:字节流只管搬运字节,它不知道、也不关心这些字节代表什么文字编码信息需要程序员自己记住,并在每次解码时显式指定,一旦忘掉或者猜错,就会产生乱码;
  2. 多字节字符边界处理复杂:UTF-8 中,一个中文字符占 3 个字节,如果逐字节读取,第 100 个字节可能正好切在一个汉字中间,自己拼字符串时还得判断边界;
  3. 每次处理都要重复编码/解码逻辑:每次从文件读字节,都要 new String(bytes, charset);每次写入,都要 string.getBytes(charset)

a.字符流解决编码信息丢失:

java
// 写:构造时绑定编码,后续直接写字符串 try (FileWriter fw = new FileWriter("test.txt", StandardCharsets.UTF_8)) { fw.write("你好世界"); } // 读:构造时绑定编码,后续直接读字符 try (FileReader fr = new FileReader("test.txt", StandardCharsets.UTF_8)) { char[] buffer = new char[1024]; int len = fr.read(buffer); System.out.println(new String(buffer, 0, len)); // 输出: 你好世界 ✅ }

编码信息被绑定在流对象,从JVM层面来讲也就是堆上,真正的被JVM所记住,读取时不需要手动的new String(bytes, charset),编码变成了“对象的状态或者说属性”;

b.字符流解决多字节字符边界处理问题:

java
/** * 准备测试文件:包含中文字符 * '你' = E4 BD A0(3字节),'好' = E5 A5 BD(3字节),'世' = E4 B8 96(3字节),'界' = E7 95 8C(3字节) */ private static void prepareTestFile() throws IOException { try (FileWriter fw = new FileWriter("boundary_test.txt", StandardCharsets.UTF_8)) { fw.write("你好世界"); // 12个字节(4个汉字 × 3字节) } } /** * 直接对比:一个文件,两种读法 */ private static void testDirectComparison() throws IOException { String filePath = "boundary_test.txt"; System.out.println("=== 字节流:每次读取 1 个字节 ==="); try (InputStream is = new FileInputStream(filePath)) { int b; int count = 0; while ((b = is.read()) != -1) { count++; System.out.printf("读取第 %d 个字节: 0x%02X\n", count, b); } System.out.println("总共读取: " + count + " 个字节"); } System.out.println("\n=== 字符流:每次读取 1 个字符 ==="); try (Reader reader = new FileReader(filePath, StandardCharsets.UTF_8)) { int ch; int count = 0; while ((ch = reader.read()) != -1) { count++; System.out.printf("读取第 %d 个字符: '%c'\n", count, (char) ch); } System.out.println("总共读取: " + count + " 个字符"); // 输出: 4 个字符 } }
shell
输出结果 === 字节流:每次读取 1 个字节 === 读取第 1 个字节: 0xE4 读取第 2 个字节: 0xBD 读取第 3 个字节: 0xA0 读取第 4 个字节: 0xE5 读取第 5 个字节: 0xA5 读取第 6 个字节: 0xBD 读取第 7 个字节: 0xE4 读取第 8 个字节: 0xB8 读取第 9 个字节: 0x96 读取第 10 个字节: 0xE7 读取第 11 个字节: 0x95 读取第 12 个字节: 0x8C 总共读取: 12 个字节 === 字符流:每次读取 1 个字符 === 读取第 1 个字符: '你' 读取第 2 个字符: '好' 读取第 3 个字符: '世' 读取第 4 个字符: '界' 总共读取: 4 个字符

这样对比一目了然: 同一个文件,字节流读出了 12 个"碎片",字符流读出了 4 个"完整字符"。

c. 字符流解决字节流每次处理都要重复编码/解码逻辑,这个主要是处理重复代码本质上和问题一很相似

java
/** * 用字节流复制文本文件 * 注意:到处都在写 getBytes() 和 new String() */ public static void copyByByteStream(String src, String dest) throws IOException { // 读:字节 → 字符串,要 new String() try (InputStream is = new FileInputStream(src)) { byte[] bytes = is.readAllBytes(); String content = new String(bytes, StandardCharsets.UTF_8); // 手动解码 // 写:字符串 → 字节,要 getBytes() try (OutputStream os = new FileOutputStream(dest)) { os.write(content.getBytes(StandardCharsets.UTF_8)); // 手动编码 } } } /** * 用字符流复制文本文件 * 注意:没有任何 getBytes() 和 new String() */ public static void copyByCharStream(String src, String dest) throws IOException { try (Reader reader = new FileReader(src, StandardCharsets.UTF_8); Writer writer = new FileWriter(dest, StandardCharsets.UTF_8)) { char[] buffer = new char[8192]; int len; while ((len = reader.read(buffer)) != -1) { writer.write(buffer, 0, len); // 直接写字符数组,不用手动转换 } } }

1.3.3 内存中常用的操作类

常用的IO操作类

1.ByteArrayOutPutStream、ByteArrayInputStream

java
/** * 测试把多个片段拼成一个完整的字节数组 * @throws IOException */ private static void testByteArrayOutputStream() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); //写数据,把多个片段拼成一个完整的字节数组 bos.write(65); //表示A bos.write("hello".getBytes(StandardCharsets.UTF_8)); bos.write("world".getBytes(StandardCharsets.UTF_8)); byte[] result = bos.toByteArray(); System.out.println(new String(result)); String str = bos.toString(StandardCharsets.UTF_8); System.out.println(str); //写入到文件当中 bos.writeTo(new FileOutputStream(filePath)); } /** * 模拟文件复制 */ private static void testCopy() throws IOException { //准备源数据 String source = "Hello, ByteArrayStream!"; byte[] srcData = source.getBytes(StandardCharsets.UTF_8); // 从内存读取 ByteArrayInputStream bais = new ByteArrayInputStream(srcData); // 写入内存(目标) ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 复制 byte[] buffer = new byte[1024]; int len; while ((len = bais.read(buffer)) != -1) { baos.write(buffer, 0, len); } // 模拟输出结果 System.out.println(baos.toString(StandardCharsets.UTF_8)); }

2.StringReader,StringWriter

java
private static void testStringReader(){ StringWriter sw = new StringWriter(); //用字符流的方式写入字符串 sw.write("第一行\n"); sw.write("第二行\n"); sw.write("第三行"); //也可以配合 PrintWriter 使用 PrintWriter pw = new PrintWriter(sw); pw.println("这是用PrintWriter写的"); pw.printf("格式化数字:%d\n", 123); pw.close(); // 注意关闭 PrintWriter,但 StringWriter 不用关 // 最终取出完整的字符串 String result = sw.toString(); System.out.println(result); // 也可以获取内部的 StringBuffer StringBuffer sb = sw.getBuffer(); System.out.println("StringBuffer内容:" + sb.toString()); } /** * 测试模板替换 */ private static void testTemplateProcessor() throws IOException { // 模板内容(可以来自配置文件、数据库等) String template = """ 尊敬的 {name}: 您的订单 {orderId} 已发货,预计 {days} 天内送达。 感谢您的购买! """; // 用 StringReader 读取模板 StringReader sr = new StringReader(template); BufferedReader br = new BufferedReader(sr); // 用 StringWriter 构建结果 StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); // 逐行处理替换 String line; while ((line = br.readLine()) != null) { line = line.replace("{name}", "李四") .replace("{orderId}", "20240830-001") .replace("{days}", "3"); pw.println(line); } pw.close(); // 输出结果 System.out.println(sw.toString()); }

1.3.4 线程间通信与IO有何关联

       线程是CPU工作的最小单元,它们之间既能相互协作,也能相互竞争。那么,如何实现线程间的通信呢?这里我们从IO的角度,来看看Java如何让线程通过IO进行通信。

java
public static void main(String[] args) throws IOException { // 创建管道:一个输出端,一个输入端 PipedInputStream ins = new PipedInputStream(); PipedOutputStream ops = new PipedOutputStream(ins); //生产者写入消息到管道流 Thread producer =new Thread(() -> { try (PrintWriter pw = new PrintWriter(ops)) { pw.println("Hello 我是生产者"); pw.println("This is a message through pipe."); } }); //消费者读取 Thread consumer = new Thread(() -> { try (BufferedReader br = new BufferedReader(new InputStreamReader(ins))) { String line; while ((line = br.readLine())!=null){ System.out.println("消费者收到:" + line); } } catch (IOException e) { throw new RuntimeException(e); } }); producer.start(); consumer.start(); }

两个线程完全不知道对方的存在,只是通过"读/写同一个管道流"来通信,IO API 被直接用来做线程间通信,数据在内存中流转,不涉及任何物理设备

1.3.5 网络Socket

Java 中的 Socket 本质上是 TCP 协议在应用层的一次面向对象封装。TCP 通信需要明确目标主机的 IP 地址和端口号,同样,建立 Socket 连接也离不开这两个网络坐标。对于服务端而言,其核心流程是通过 ServerSocket 向操作系统申请占用一个指定端口,随后调用 accept() 方法进入阻塞等待状态——这本质上是一次系统级调用,线程在此挂起,直到 TCP 三次握手成功、客户端连接到达,才会被唤醒并继续执行。正是这种“阻塞监听”模型,奠定了 Java 传统 BIO 通信的基础。

一个简单的案例,客户端像服务端发送消息:

java
/** * 服务端 */ private static final int PORT = 9527; public static void main(String[] args) { System.out.println("========== 服务器启动 =========="); System.out.println("监听端口: " + PORT); ServerSocket socket = new ServerSocket(PORT); try { while (true){ Socket accept = socket.accept(); SocketAddress remoteSocketAddress = accept.getRemoteSocketAddress(); System.out.println("新链接:"+remoteSocketAddress.toString()); //broadcast(remoteSocketAddress,accept); 广播 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(accept.getInputStream())); String line = ""; while ((line=bufferedReader.readLine())!=null){ System.out.println("收到客户端消息:"+line); } } } catch (IOException e) { throw new RuntimeException(e); } }
java
/** * 客户端 */ private static final int PORT = 9527; public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1",PORT); //获取键盘输入 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); System.out.print("请输入: "); PrintWriter out = new PrintWriter(socket.getOutputStream(),true); String line = ""; while ((line=bufferedReader.readLine())!=null){ //发送给服务端 out.println(line); } } catch (IOException e) { System.out.println("断开链接"); throw new RuntimeException(e); } }

在服务端加一个广播的功能,在客户端加一个接收服务端消息的功能

java
/** * 广播消息给所有在线链接 */ private static void broadcast(SocketAddress address,Socket socket) throws IOException { String line = "新链接"+address+"已连接"; PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true); printWriter.println(line); }
java
public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1",PORT); //读取服务器信息 BufferedReader serverReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(()->{ String readMessage = ""; try { while ((readMessage = serverReader.readLine())!=null){ System.out.println(readMessage); } }catch (Exception e){ } }); //从键盘获取信息发送给服务端 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); System.out.println("请输入: "); PrintWriter out = new PrintWriter(socket.getOutputStream(),true); String line = ""; while ((line=bufferedReader.readLine())!=null){ out.println(line); } } catch (IOException e) { System.out.println("断开链接"); throw new RuntimeException(e); } }

客户端控制台:

shell
请输入: 新链接/127.0.0.1:49219已连接 你好

服务端控制台:

shell
========== 服务器启动 ========== 监听端口: 9527 新链接:/127.0.0.1:49219 收到客户端消息:你好

1.4 装饰器模式

从上面的测试可以看出,想要提升 I/O 性能,几乎离不开 BufferedInputStream / BufferedReader 这类缓冲流。这正是 Java I/O 设计中装饰器模式的典型应用: 功能与数据源分离:缓冲、加密、压缩等增强逻辑,与底层数据来源(文件、网络、内存)完全解耦 自由组合:想要缓冲就套 BufferedXXX,想要加密就再套一层 CipherInputStream,想要压缩就继续套 GZIPInputStream——每一层只负责一件事,层层叠加,按需组装 如下图(插入装饰器模式图):

┌─────────────────────┐ │ InputStream │ ← 统一抽象 └──────────┬──────────┘ ┌───────────┬─────────┼─────────┬───────────┐ ↓ ↓ ↓ ↓ ↓ FileInputStream ByteArray Socket Piped ... ← 数据源(叶子节点) ↑ ↑ ↑ ↑ └───────────┴─────────┼─────────┘ ↓ ┌─────────────────────┐ │ BufferedInputStream │ ← 装饰器(增强) └──────────┬──────────┘ ↓ ┌─────────────────────┐ │ CipherInputStream │ ← 装饰器(增强) └──────────┬──────────┘ ↓ 你想要的任意组合

一句话来说就是:**装饰器模式是一种结构型设计模式,它允许在不修改对象结构的情况下,动态地为对象添加

二、总结

写完这篇博客,回头再看 Java I/O,感受和几年前初学的时候完全不一样了。当时是为了应付面试硬背概念、API,现在更多在思考“为什么这么设计”。几个比较深的感触,记录一下。

  1. 字节流 vs 字符流

以前老是记混什么时候用 InputStream、什么时候用 Reader,经过这轮回看,心里总算有了杆秤:

对于文件是图片、视频、音频、或者序列化后的二进制数据,字节流。因为这些内容根本没有“字符”这个概念,用字符流反而会破坏数据。字节流是万能的,什么都能读,只是读出来的是 bytes,得自己处理。

纯文本文件,或者要逐行读取日志、JSON、配置文件,对应字符流;

  1. 装饰器模式

它最妙的地方在于:你不需要知道数据是从哪里来的。

给文件读加缓冲,和给 Socket 网络流加缓冲,写法一模一样——都是 new BufferedInputStream(whateverInputStream)。缓冲层只关心“我前面有个 InputStream”,它不关心这个 InputStream 是来自硬盘、内存、网络还是另一个线程。加密、压缩、序列化这些增强功能也是一样,想加哪层加哪层,每一层各司其职,互不干扰。

这让我对 Java 的设计模式产生了浓厚的兴趣——一个好的架构设计,是真的能让程序员少掉很多头发。后面我打算系统地把常用的设计模式再过一遍,结合源码去理解。

  1. Socket 与 BIO:从阻塞到非阻塞,Java 也在进化

关于Socket,有两层感受是交织在一起的。

第一层是赞叹:Java 对 TCP 的封装真的太方便了。ServerSocket 和 Socket 把三次握手、端口绑定、数据收发这些底层细节全部屏蔽掉了,配合 I/O 流的装饰器模式,几行代码就能搭一个能用的服务端。这对于应用层开发者来说,是极大的解放。

第二层其实是疑惑:accept()和read()一阻塞,每条连接就得霸着一个线程,在高并发下服务器根本招架不住。于是我了解到了NIO,发现它很好的弥补了这一点,让我想起字节流和字符流:前者处理文本时要手动编解码,对开发者极不友好,于是后者应运而生。 这种从I/O到并发,从反射到模块化,JDK已走到26,二十多年来Java可能一直在循环做着同一件事:解决旧问题,暴露新问题,再被下一代版本化解。可能这就是它的生命力吧,如今AI时代,Java又会变成什么样呢?

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Yulu

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!