欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。

✨✨ 欢迎订阅本专栏 ✨✨

博客目录

一.基础1.IO 发展历程2.I/O 请求3.BIO4.NIO5.IO 多路复用6.信号驱动7.异步 IO8.NIO 三大组件9.事件10.阻塞和非阻塞11.Java 中有几种类型的流12.字节流如何转为字符流13.字节流和字符流的区别14.序列化和 IO 的关系15.BIO 和 NIO 的概念16.异常机制的过程17.异常体系18.异常输出打印的常用方法19.Throw 和 Throws 的区别20.try-with-resources 替代 try-catch-finally21.读文件行

二.ByteBuffer1.ByteBuffer 结构2.ByteBuffer 实现3.分配空间4.向 buffer 写入数据5.从 buffer 读取数据6.mark 和 reset7.字符串与 ByteBuffer 互裝8.Buffer 相关优化9.处理消息边界10.ByteBuffer 大小分配11.ByteBuffer 正确使用姿势

三.channel1.stream 和 channel

四.selector1.监听 Channel 事件2.selector 和 selectedKeys3.select 何时不阻塞4.如何拿到 cpu 个数5.利用多线程优化6.wakeup

五.文件编程1.FileChannel2.获取3.读取4.写入5.位置6.大小7.Path8.Files9.拷贝文件10.移动文件11.删除文件12.删除目录

六.IO 模型1.同步异步2.IO 模型3.阻塞 IO4.非阻塞 IO5.多路复用6.异步 IO

七.零拷贝1.IO 读写的基本原理2.传统 IO3.NIO 优化4.sendFile 优化5.进一步优化

八.多路复用1.什么是 IO 多路复用?2.select 模型?3.poll 模型?4.epoll 模型?

一.基础

1.IO 发展历程

在 JDK1.4 投入使用之前,只有 BIO 一种模式JDK1.4 以后开始引入了 NIO 技术,支持 select 和 pollJDK1.5 支持了 epollJDK1.7 发布了 NIO2,支持 AIO 模型

2.I/O 请求

I/O 调用阶段:用户进程向内核发起系统调用 I/O 执行阶段:内核等待 I/O 请求处理完成返回

3.BIO

4.NIO

5.IO 多路复用

6.信号驱动

7.异步 IO

8.NIO 三大组件

NIO 是 non-blocking IO 非阻塞 IO

会详细讲解 NIO 的 Selector、ByteBuffer 和 Channel 三大组件。

Channel

channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将

buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层

常见的 Channel 有

FileChannelDatagramChannelSocketChannelServerSocketChannel

buffer

buffer 则用来缓冲读写数据,常见的 buffer 有

ByteBuffer

MappedByteBufferDirectByteBufferHeapByteBuffer ShortBufferIntBufferLongBufferFloatBufferDoubleBufferCharBuffer

Selector

selector 单从字面意思不好理解,需要结合服务器的设计演化来理解已的用途

服务器设计

多线程版本

多袋程版缺点

内存占用高线程上下文切换成本高只适合连接数少的场景

线程池版本

线程池版缺点

阻塞模式下,线程仅能处理一个 socket 连接仅适合短连接场景

selector 版设计

selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景 (low traffic)

调用 selector 的 select 方法会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理

9.事件

accept -会在有连接请求时触发 connect -是客户端,连接建立后触发 read - 可读事件 wite-可写事件

10.阻塞和非阻塞

阻塞

阻塞模式下,相关方法都会导致线程暂停

ServerSocketChannel.accept 会在没有连接建立时让线程暂停SocketChannel.read 会在没有数据可读时让线程暂停阻塞的表现其实就是线程暂停了,暂停期间不会占用 Cpu,但线程相当于闲置 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持 但多线程下,有新的问题,体现在以下方面

32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 00M,并且线程太多,反而会因为频繁上下文切换导致性能降低 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

非阻塞

在某个 Channel 没有可读事件时,线程不必阻塞,它可以去处理其它有可读事件的 Channel数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去

11.Java 中有几种类型的流

按照流的流向分:输入流(inputStream)和输出流(outputStream)按照操作单元划分:字节流和字符流按照流的角色功能划分:节点流和处理流。

节点流:可以从或向一个特定的地方(节点)读写数据。如 FileReader处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。

12.字节流如何转为字符流

字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。

字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象.

13.字节流和字符流的区别

字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符字符数组或字符串,而字节流处理单元为 1 个字节, 操作字节和字节数组。所以字符流是由 Java 虚拟机将字节转化为 2 个字节的 Unicode 字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点

所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成字节序列。

字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串

字节流提供了处理任何类型的 IO 操作的功能,但它不能直接处理 Unicode 字符,而字符流就可以。

14.序列化和 IO 的关系

[什么是 Java 序列化,如何实现 java 序列化](https://www.cnblogs.com/yangchunze/p/6728086.html)

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。

序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 ,

implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个 ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。

实体类

public class Customer implements Serializable {

private String name;

private int age;

public Customer(String name, int age) {

this.name = name;

this.age = age;

}

@Override

public String toString() {

return "name=" + name + ", age=" + age;

}

}

测试类

public class Test {

public static void main(String[] args) throws Exception {

/*其中的 D:\\objectFile.obj 表示存放序列化对象的文件*/

// 序列化对象

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\objectFile.obj"));

Customer customer = new Customer("BWH_Steven", 22);

out.writeObject("Hello!"); // 写入字面值常量

out.writeObject(new Date()); // 写入匿名Date对象

out.writeObject(customer); // 写入customer对象

out.close();

// 反序列化对象

ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\objectFile.obj"));

System.out.println("obj1 " + (String) in.readObject()); // 读取字面值常量

System.out.println("obj2 " + (Date) in.readObject()); // 读取匿名Date对象

Customer obj3 = (Customer) in.readObject(); // 读取customer对象

System.out.println("obj3 " + obj3);

in.close();

}

}

运行结果

// 实体类实现 Serializable 接口

obj1 Hello!

obj2 Sat Feb 06 11:17:57 CST 2021

obj3 name=BWH_Steven, age=22

// 实体类不实现 Serializable 接口

Exception in thread "main" java.io.NotSerializableException: cn.ideal.pojo.Customer

at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

at cn.ideal.Test.main(Test.java:27)

15.BIO 和 NIO 的概念

[关于 BIO 和 NIO 的理解](https://www.cnblogs.com/zedosu/p/6666984.html)

16.异常机制的过程

异常就是在程序发生异常时,强制终止程序运行,并且将异常信息返回,由开发者决定是否处理异常

简单说一下这个异常机制的过程:

当程序无法运行后,它会从当前环境中跳出,并且抛出异常,之后,它会先 new 一个异常对象,然后在异常位置终止程序,并且将异常对象的引用从当前环境中返回,这时候异常处理机制接管程序,并且开始寻找可以继续执行程序的恰当位置。

17.异常体系

Error —— 错误:程序无法处理的严重错误,我们不作处理

这种错误一般来说与操作者无关,并且开发者与应用程序没有能力去解决这一问题,通常情况下,JVM 会做出终止线程的动作 Exception —— 异常:异常可以分为运行时异常和编译期异常

RuntimeException:即运行时异常,我们必须修正代码

这些异常通常是由于一些逻辑错误产生的这类异常在代码编写的时候不会被编译器所检测出来,是可以不需要被捕获,但是程序员也可以根据需要行捕获抛出,(不受检查异常)这类异常通常是可以被程序员避免的。 常见的 RUNtimeException 有:NullpointException(空指针异常),ClassCastException(类型转 换异常),IndexOutOfBoundsException(数组越界异常)等。 非 RuntimeException:编译期异常,必须处理,否则程序编译无法通过 这类异常在编译时编译器会提示需要捕获,如果不进行捕获则编译错误。 常见编译异常有:IOException(流传输异常),SQLException(数据库操作异常)等。

18.异常输出打印的常用方法

方法方法说明public String getMessage()回关于发生的异常的详细信息。这个消息在 Throwable 类的构造函数中初始化了public Throwable getCause()返回一个 Throwable 对象代表异常原因public String toString()使用 getMessage()的结果返回类的串级名字public void printStackTrace()打印 toString()结果和栈层次到 System.err,即错误输出流

示例:

public class Demo {

public static void main(String[] args) {

int a = 520;

int b = 0;

int c;

try {

System.out.println("这是一个被除数为0的式子");

c = a / b;

} catch (ArithmeticException e) {

System.out.println("除数不能为0");

}

}

}

//运行结果

这是一个被除数为0的式子

除数不能为0

我们用上面的例子给出异常方法的测试

// System.out.println(e.getMessage()); 结果如下:

/ by zero

// System.out.println(e.getCause()); 结果如下:

null

// System.out.println(e.toString()); 结果如下:

java.lang.ArithmeticException: / by zero

// e.printStackTrace(); 结果如下:

java.lang.ArithmeticException: / by zero

at cn.bwh_01_Throwable.Demo.main(Demo.java:10)

19.Throw 和 Throws 的区别

Throw:

作用在方法内,表示抛出具体异常,由方法体内的语句处理。 具体向外抛出的动作,所以它抛出的是一个异常实体类。若执行了 Throw 一定是抛出了某种异常。

Throws:

作用在方法的声明上,表示如果抛出异常,则由该方法的调用者来进行异常处理。 主要的声明这个方法会抛出会抛出某种类型的异常,让它的使用者知道捕获异常的类型。 出现异常是一种可能性,但不一定会发生异常。

20.try-with-resources 替代 try-catch-finally

面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是 try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。try-with-resources 语句让我们更容易编写必须要关闭的资源的代码,若采用 try-finally 则几乎做不到这点。—— Effecitve Java

Java 从 JDK1.7 开始引入了 try-with-resources ,在其中定义的变量只要实现了 AutoCloseable 接口,这样在系统可以自动调用它们的 close 方法,从而替代了 finally 中关闭资源的功能。

使用 try-catch-finally 你可能会这样做

try {

// 假设这里是一组关于 文件 IO 操作的代码

} catch (IOException e) {

e.printStackTrace();

} finally {

if (s != null) {

s.close();

}

}

但现在你可以这样做

try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("test.txt")))) {

// 假设这里是操作代码

} catch (IOException e) {

e.printStackTrace();

}

如果有多个资源需要 close ,只需要在 try 中,通过分号间隔开即可

try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("test.txt")));

BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File("test.txt")))) {

// 假设这里是操作代码

} catch (IOException e) {

e.printStackTrace();

}

21.读文件行

List lines = Files.readAllLines(Paths.get(dicPath));

二.ByteBuffer

1.ByteBuffer 结构

ByteBuffer 有以下重要属性

capacitypositionlimit

写模式下,position 是写入位置,limit 等于可写容量,capacity 是最大容量

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制

clear 动作发生后,状态

compact 方法,是把未读完的部分向前压缩,然后切换至写模式

下面是一个简单的图形化展示,展示了一个 ByteBuffer 对象的 position、limit 和 capacity 属性的关系:

+-------------------+------------------+------------------+

| capacity | limit | position |

+-------------------+------------------+------------------+

| | | |

| | | |

| |<---- remaining --->|<---- remaining --->|

| | | |

| | | |

| |<--------- buffer --------->| |

| | | |

| | | |

| |<---- flip() ------>|<---- flip() ------>|

| | | |

| | | |

| |<------ clear() ------->| |

| | | |

+-------------------+------------------+------------------+

上面的图形展示了一个 ByteBuffer 对象,它具有以下属性:

capacity:表示 ByteBuffer 对象的容量,即它可以包含多少字节。limit:表示 ByteBuffer 对象中可读取或可写入的字节数。它始终小于或等于 capacity 属性。position:表示当前读取或写入操作所处的位置。初始时,position 的值为 0,它始终小于或等于 limit 属性。

在创建一个 ByteBuffer 对象后,可以使用 put() 方法向其中写入数据。当写入完成后,需要调用 flip() 方法将 limit 属性设置为当前 position 的值,并将 position 的值重置为 0,以便读取数据。读取完成后,可以调用 clear() 方法将 limit 和 position 的值重置为初始值,以便重新写入数据。

需要注意的是,当读取或写入数据时,position 属性的值会随着操作的进行而自动增加。因此,在读取或写入数据之前,需要记录当前 position 的值,以便在操作完成后将 position 的值恢复到原始值。

2.ByteBuffer 实现

class java.nio.HeapByteBuffer:java 堆内存,读写效率较低,受到 GC 的影响 class java.nio.DirectByteBuffer:直接内存,读写效率高(少一次拷贝),不会受 GC,分配的效率低

3.分配空间

可以使用 allocate 方法为 ByteBuffer 分配空间,其它 buffer 类也有该方法

ByteBuffer.allocate(16);

4.向 buffer 写入数据

有两种办法

调用 channel 的 read 方法调用 buffer 自己的 put 方法

#方式一

buffer.put(new byte[]{0x62, 0x63, 0x64});

#方式二

channel.read(buffer);

5.从 buffer 读取数据

同样有两种办法

调用 channel 的 write 方法调用 buffer 自己的 get 方法

#方式一

int writeBytes = channel.write(buf);

#方式二

byte b =buf.get();

set 方法会让 position 读指针向后走,如果想重复读取数据

可以调用 rewind 方法将 position 重新置为 0或者调用 get(int)) 方法获取索引 i 的内容,它不会移动读指针

get(i)不会改交读索引的位置 System.out.println((char) buffer.get(3));

6.mark 和 reset

mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置

mark 微一个标记,记柔 position 位置,reset 是将 position 重置到 mark 的位置

7.字符串与 ByteBuffer 互裝

ByteBuffer bufferl = Standar dCharsets.UTF_8. encode("你好");

Bytesuffer buffer2 = Charset. forName("utf-8")encode("休好"):

debug (buffer1);

debug (buffer2);

CharBuffer buffer3 = Standardcharsets. UTF_8. decode (buffer 1);

System.out.print]n (buffer3.getClass ());

System.out.print]n(buffer3.toString());

8.Buffer 相关优化

ChannelBuffer 变更为 ByteBuf,Buffer 相关的工具类可以独立使用Buffer 统一为动态变化,更安全地更改 Buffer 的容量增加新的数据类型 CompositeByteBuf,用于减少数据拷贝GC 更加友好,增加池化缓存,4.1 版本开始 jemalloc 成为默认内存分配方式内存泄漏检测功能

9.处理消息边界

一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽另一种思路是按分隔符拆分,缺点是效率低TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量

Http1.1 是 TLV 格式Http2.0 是 LTV 格式

10.ByteBuffer 大小分配

每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不是线程安全的,因此需要为每个 channel 维 护一个独立的 ByteBuffer ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小 可变的 ByteBuffer

解决方案

一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消 息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗

11.ByteBuffer 正确使用姿势

向 buffer 写入数据,例如调用 channel.read(buffer)调用 flip()切换至读模式从 buffer 读取数据,例如调用 buffer.get()调用 clear()或 compact()切换至写模式重复 1 ~ 4 步骤

三.channel

1.stream 和 channel

stream 不会自动缓冲数据,channe 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)stream 仅支持阻塞 APl,channel 同时支持阻塞、非阻塞 APl,网络 channel 可配合 selector 实现多路复用二者均为全双工,即读写可以同时进行

四.selector

1.监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

//方法1,阻塞直到绑定事件发生

int count = selector.select(O);

//方法2,阻塞直到绑定事件发生,或是超时(时间单位为ms)

int count = selector.select(long timeout);

//方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int countT= selector.selectNow();

2.selector 和 selectedKeys

3.select 何时不阻塞

事件发生时

客户端发起连接请求,会触发 accept 事件客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件channel 可写,会触发 write 事件在 linux 下 nio bug 发生时 调用 selector.wakeup()调用 selector.close()selector 所在线程 interrupt

4.如何拿到 cpu 个数

Runtime.getRuntime().availableProcessors()如果工作在 docker 容器下,因为容器不是物理隔离的, 会拿到物理 cpu 个数,而不是容器申请时的个数 这个问题直到 jdk 10 才修复,使用 jvm 参数 UseContainerSupport 配置,默认开启

5.利用多线程优化

现在都是多核 cpu,设计时要充分考虑别让 cpu 的力量被白白浪费,前面的代码只有一个选择器,没有充分利用多核 cpu,如何改进呢?

分两组选择器:

单线程配一个选择器,专门处理 accept 事件创建 cpu 核心数的线程,每个线程配一个选择器,轮流处理 read 事件

6.wakeup

selector.wakeup()//唤醒 select 方洗 boss

selector.select()//worker-0 阻塞

sc.register(selector, SelectionKey.OP_READ, null); // boss

因为 wakeup 方法的特性,即使提前唤醒,也不会在 select 方法阻塞

五.文件编程

1.FileChannel

FileChannel 只能工作在阻塞模式下

2.获取

不能直接打开 FileChannel,必须通过 FilelnputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

通过 FilelnputStream 获取的 channel 只能读通过 FileOutputStream 获取的 channel 只能写通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

3.读取

会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾

int readBytes = channel.read(buffer);

4.写入

写入的正确姿势如下

ByteBuffer buffer = .……;

buffer.put(...);//存入数据

buffer.flip();//切换读模式

while(buffer.hasRemaining()){

channel.write(buffer);

}

在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel

5.位置

//获取当前位置

long pos = channel.position();

//设置当前位置

long newPos =.....;

channel.position(newPos);

设置当前位置时,如果设置为文件的末尾

这时读取会返回-1这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞

6.大小

使用 size 方法获取文件的大小

强制写入:操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true)方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

7.Path

jdk7 引入了 Path 和 Paths 类

Path 用来表示文件路径Paths 是工具类,用来获取 Path 实例

Path source =Paths.get("1.txt");//相对路径使用 user.dir环境变量来定位1.txt

Path source =Paths.get("d:\\1.txt");//绝对路径代表了 d:\1.txt

Path source=Paths.get("d:/1.txt");//绝对路径同样代表了 d:\1.txt

Path projects =Paths.get("d:\\data","projects");//代表了 d:\data\projects

.代表了当前路径…代表了上一级路径

例如目录结构如下 d: |- data I- projects |-a |-b

Path path = Paths.get ("d:\\data\ \projects\la\\.. \\b");

System.out.println(path);

system.out.println(path.normalize();//正常化路径

//输出结果

d:\data\projects\a\.. \b

d: data projects\b

8.Files

检查文件是否存在

Path path = Paths.get("helloword/data.txt");

System.out.println(Files.exists(path));

创建一级目录

Path path = Paths.get("helloword/d1");

Files.createDirectory(path);

如果目录已存在,会抛异常 FileAlreadyExistsException不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录用

Path path = Paths.get("helloword/d1/d2");

Files.createDirectories(path)

9.拷贝文件

Path source = Paths.get("helloword/data.txt");

Path target = Paths.get("helloword/target.txt");

Files.copy(source, target);

如果文件已存在,会抛异常 FileAlreadyExistsException如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

10.移动文件

Path source = Paths.get("helloword/data.txt");

Path target = Paths.get("helloword/data.txt");

Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);

StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性

11.删除文件

Path target = Paths.get("helloword/target.txt");

Files.delete(target);

如果文件不存在,会抛异常 NoSuchFileException

12.删除目录

Path target = Paths.get("helloword/d1");

Files.delete(target);

如果目录还有内容,会抛异常 DirectoryNotEmptyException

六.IO 模型

1.同步异步

同步阻塞同步非阻塞多路复用异步阻塞:不存在的类型异步非阻塞

同步异步:

同步:线程自己去获取结果(一个线程)异步:线程自己不去获取结果,而是由其它线程送结果(至少两个线程)

2.IO 模型

阻塞 IO非阻塞 IO多路复用信号驱动异步 IO

当调用一次 channel.read 或 stream.read 后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个 阶段,分别为:

等待数据阶段复制数据阶段

3.阻塞 IO

4.非阻塞 IO

5.多路复用

6.异步 IO

AIO 用来解决数据复制阶段的阻塞问题

同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获 得结果

异步模型需要底层操作系统(Kernel)提供支持

Windows 系统通过 IOCP 实现了真正的异步 IOLinux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势

七.零拷贝

1.IO 读写的基本原理

为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为两部分:一部分是内核空间 (Kernel-Space),另一部分是用户空间(User-Space)。在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;用户程序运行在用户空间,对应的进程处于用户态。

操作系统的核心是内核程序,它独立于普通的应用程序,既有权限访问受保护的内核空间,也有权限访问硬件设 备,而普通的应用程序并没有这样的权限。内核空间总是驻留在内存中,是为操作系统的内核保留的。应用程序不 允许直接在内核空间区域进行读写,也不允许直接调用内核代码定义的函数。每个应用程序进程都有一个单独的用 户空间,对应的进程处于用户态,用户态进程不能访问内核空间中的数据,也不能直接调用内核函数,因此需要将 进程切换到内核态才能进行系统调用。内核态进程可以执行任意命令,调用系统的一切资源,而用户态进程只能执行简单的运算,不能直接调用系统资源

那么问题来了:用户态进程如何执行系统调用呢?

答案是:用户态进程必须通过系统调用(System Call)向内核发出指令,完成调用系统资源之类的操作。

2.传统 IO

传统 IO 问题:传统的 lO 将一个文件通过 socket 写出

File f = new File("helloword/data.txt");

RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.Tength()];

file.read(buf);

Socket socket =...;

socket.getOutputStream().write(buf);

内部工作流程是这样的:

java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,期间也不会使用 cpu 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无 法利用 DMA 调用 write 方法,这时将数据从用户缓冲区(bytel[] buf)写入 socket 缓冲区,cpu 会参与拷贝 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能 力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO

可以看到中间环节较多,java 的 lO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统 来完成的

用户态与内核态的切换发生了 3 次,这个操作比较重量级数据拷贝了共 4 次

3.NIO 优化

通过 DirectByteBuff

ByteBuffer.allocate(10) HeapByteBufferByteBuffer.allocateDirect(10) DirectByteBuffer

大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuffer 将堆外内存映射到 jvm 内存中来 直接访问使用

这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写java 中的 DirectByteBuffer 对象仅维护了此内存的虚引用,内存回收分成两步

DirectByteBuffer 对象被垃圾回收,将虚引用加入引用队列通过专门线程访问引用队列,根据虚引用释放堆外内存 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

4.sendFile 优化

进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据

java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA 将数据读入内核缓冲区,不会使用 cpu数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到

只发生了一次用户态与内核态的切换数据拷贝了 3 次

5.进一步优化

进一步优化(linux2.4)

java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA 将数据读入内核缓冲区,不 会使用 cpu 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗 使用 DMA 将内核缓冲区的数据写入网卡,不会使用 cpu

整个过程

仅仅只发生了一次用户态与内核态的切换数据拷贝了 2 次所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 ivm 内存中,

零拷贝的优点有

更少的用户态与内核态的切换不利用 cpu 计算,减少 cpu 缓存伪共享零拷贝适合小文件传输

八.多路复用

1.什么是 IO 多路复用?

IO多路复用:一种同步的 IO 模型。利用 IO 多路复用模型可以实现一个线程监视多个文件句柄,一旦某个文件句柄就绪,就能够通知到对应应用程序进行相应的读写操作;没有文件句柄就绪时就会阻塞应用程序,从而释放出 CPU 资源

IO:在操作系统中,数据在内核态和用户态之间的读写操作 多路:大部分情况下是指多个 TCP 连接(多个 Socket 或者多个 Channel) 复用:一个或多个线程资源 IO多路复用:一个或多个线程处理多个 TCP 连接。无需创建和维护过多的进程/线程

实现 IO 多路复用的模型有三种:

selectpollepoll

2.select 模型?

采用轮训加遍历的方式,在客户端操作服务器时,会创建三种文件描述符,简称 FD

分别是写描述符,读描述符,异常描述符.select 会阻塞和监视这三种文件描述符.等到有数据可读,可写,或者出异常,或者超时的时候,都会返回.返回后通过遍历 fdset(文件描述符集合),来找到就绪的 fd,然后去触发相应的 IO 操作.

优点:跨平台支持性好,几乎在所有的平台上支持 缺点:随着 FD 数量增多而导致性能下降。而操作系统对单个进程打开的 FD 数量是有限制的,一般默认是 1024 个

3.poll 模型?

采用轮训加遍历的方式,poll 模式使用链表的方式来存储 fd,优点是没有最大的 fd 的数量限制.缺点和 select 一样,采用轮训的方式来进行全盘扫描,随着 fd 数量的增加,导致性能下降

4.epoll 模型?

epoll 模型解决了 select 和 poll 因为吞吐量的增加而性能下降的问题.采用时间通知机制来触发 IO 操作.它没有 fd 个数的限制,而且从用户态拷贝到内核态只需要一次,因为它主要是通过调用系统底层的函数,来实现注册,激活 fd,这样大大提高了执行性能.主要是以下三个系统函数:

epoll_create():在系统启动时,去申请一个 B+树结构的文件系统,然后再返回 epoll 对象.也就是一个 fd 对象epoll_ctl():在每新建一个连接的时候,会同步更新 epoll 对象中的 fd,并且去绑定一个 callback 函数,epoll_wait():轮训所有的 callback 集合,并且去触发对应的 IO 操作

优点:将轮询改成了回调,大大提高了 CPU 执行效率,也不会随 FD 数量的增加而导致效率下降 缺点:只能在 Linux 下工作

❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!

如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!  

Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!

参考文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: