作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦
千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者
前言
在上一篇文章中,壹哥给大家介绍了BIO模型,我们知道在BIO模型中存在着诸多的弊端,比如线程开销大、同步阻塞IO导致IO效率低下、可靠性较差、无法实现真正的高并发等。总之,由于BIO模型在高并发访问情况下存在的性能瓶颈,无法实现真正的高并发,无法满足现代互联网高并发的需求。所以在新的需求中,我们需要对BIO模型进行改进,这就是NIO模型的由来。
------------------------------前戏已做完,精彩即开始----------------------------
全文大约【3200】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......
配套开源项目资料
Github: github.com/SunLtd/Lear…
Gitee: gitee.com/sunyiyi/Lea…
一. NIO-非阻塞式IO
1. NIO简介
我们知道,在BIO模式下,如果一个线程在执行IO操作时发生了阻塞,它就会一直等待,直到数据被读取或者发生了超时等错误才会终止阻塞。所以当有大量客户端连接时,服务器端就需要创建大量的线程来处理这些连接,但大量线程的创建和切换会带来极大的资源消耗和性能问题。
为了解决这个问题,JDK 1.4中引入了NIO(Non-Blocking I/O)模式,有的地方也称其为New IO,即New Input/Output。在NIO模式下,一个线程可以同时处理多个客户端连接的I/O操作,而不需要为每个客户端的连接都创建一个线程。这样就大大降低了线程创建和切换的开销,从而提高了服务器的并发性能。
NIO模式提供了与标准BIO API不同的工作方式,数据的读取和写入都是非阻塞的。当数据还没有准备好被读取时,读操作就会立即返回,这样线程就不会阻塞。而当数据准备好后,会通过Selector通知应用程序进行读取。写操作也类似,当数据无法立即被写入时,写操作会立即返回,这样线程也不会阻塞。
2. 基本概念
NIO模式下有三个核心组件,包括Channel(通道)、Buffer(缓冲区)和Selector(选择器) 。其中,Channel表示一个打开的连接,可以进行读写操作;Buffer是一个容器,用于存储数据;Selector可以监听多个Channel,当某个Channel上的数据可读或可写时,就会通知应用程序来进行处理。接下来壹哥再把这三个核心组件再分别介绍一下。
2.1 Channel通道
Channel表示一个可以进行IO操作的开放连接,如一个文件或一个网络套接字,类似于是IO流Stream。Channel通道给我们提供了一种新的IO操作方式,可以在缓冲区和IO设备之间快速高效地进行数据传输。目前Channel通道有两种类型:
- 文件通道:用于文件的读写操作,支持读写、截取、映射和锁定操作;
- 套接字通道:用于网络通信的读写操作,支持非阻塞模式和多路复用模式。
Channel通道具有如下特点:
- 可以同时进行读写操作;
- 可以进行异步地读写;
- 可以从Channel中读取数据到Buffer,也可以将Buffer中的数据写入Channel中。
目前常用的Channel API包括:
- FileChannel:用于文件的读写操作;
- DatagramChannel:基于UDP协议,进行数据的读写操作;
- SocketChannel:基于TCP协议,进行客户端的读写操作;
- ServerSocketChannel:基于TCP协议,进行服务器端的读写操作。
2.2 Buffer缓冲区
Buffer是一个容器对象,用于存储数据。在NIO中,所有的数据都是用Buffer缓冲区来处理的。缓冲区实质上是一个数组,可以保存多个数据类型,其内部有一个指针,可以指向下一个待读取或写入的位置。Buffer缓冲区有两种类型:
- 直接缓冲区:该缓冲区是在操作系统的内存中分配的,可以避免在Java堆和操作系统间的复制。
- 非直接缓冲区:该缓冲区是在Java堆中分配的,可以在与操作系统间进行数据交换时进行复制。
Buffer具有如下特点:
- 可以读写数据;
- 有一个position指针,代表下一个要读写的位置;
- 有一个limit指针,代表数据的长度,读写操作不能超出这个范围;
- 有一个capacity指针,代表Buffer的总大小。
因为Buffer是一个顶层接口,目前有以下几个具体的实现类:
- ByteBuffer:用于字节数据的存取操作;
- CharBuffer:用于字符数据的存取操作;
- ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer:用于对应数值型数据的存取操作。
Buffer的基本使用流程如下:
- 创建Buffer对象;
- 写入数据到Buffer中;
- 调用flip()方法,将Buffer从写模式切换到读模式;
- 从Buffer中读取数据;
- 调用clear()或compact()方法,将Buffer从读模式切换到写模式。
2.3 Selector选择器
Selector选择器是NIO用于检查一个或多个Channel状态是否处于可读、可写等事件的核心组件。简单来说,Selector可以管理所有打开的通道,并监听多个通道上的事件,在有事件发生时立即处理。
尤其是Selector可以实现在单线程中管理多个Channel。当一个Channel中有数据可读或可写时,Selector会自动将其加入到待处理队列中,等待Selector处理,从而可以实现多路复用,提高了程序的性能,避免了不必要的CPU消耗。
3. NIO开发步骤
如果我们想进行NIO开发,可以遵循以下几个步骤:
创建一个Selector对象; 创建一个或多个Channel对象,并将其注册到Selector中; 创建一个Buffer对象,用于存储读取到的数据; 调用Selector的select()方法,等待数据的到来; 遍历Selector的selectedKeys集合,处理已经就绪的Channel; 从Channel中读取数据到Buffer中; 调用Buffer的flip()方法进行模式切换。
接下来壹哥就通过两个案例来给大家演示如何在NIO中进行数据的读写,请大家继续往下看吧。
4. 读取数据
4.1 读取流程
NIO读取数据的流程大致如下:
创建一个Buffer对象,用于保存读取到的数据; 创建一个Channel对象,用于读取数据; 将数据从Channel读取到Buffer中; 从Buffer中读取数据并进行处理。
4.2 代码案例
下面就是壹哥设计的NIO读取数据的案例,如下所示:
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author 一一哥Sun
*/
public class Demo16 {
public static void main(String[] args) {
try {
FileInputStream fileInputStream = new FileInputStream("F:/a.txt");
// 1.创建Channel对象
FileChannel fileChannel = fileInputStream.getChannel();
// 2.设置缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 3.将数据从Channel读取到Buffer中
while (fileChannel.read(byteBuffer) != -1) {
//4.切换到读模式
byteBuffer.flip();
// 判断是否还有内容
while (byteBuffer.hasRemaining()) {
//5.读取数据并进行处理
System.out.print((char) byteBuffer.get());
}
//6.切换到写模式,为下次读取数据做准备
byteBuffer.clear();
}
// 关闭资源
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 写入数据
我们可以利用NIO实现数据的读取,当然也可以实现数据的写入。NIO提供了多种写入数据的方式,其中最常见的是通过Channel和Buffer进行写入。接下来壹哥就给大家介绍一下如何使用Channel和Buffer来写入数据。
5.1 写入流程
NIO写入数据的流程大致如下:
创建一个Buffer缓冲区对象,用于保存要写入的数据; 创建一个Channel对象,用于写入数据; 将数据从Channel写入到Buffer中; 关闭释放资源。
接下来壹哥就讲解一下写入数据的代码实现。
5.2 代码案例
利用NIO写入数据的代码案例如下所示:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* @author 一一哥Sun
*/
public class Demo17 {
public static void main(String[] args) {
//1.创建一个Channel对象
Path path = Paths.get("F:/d.txt");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
//2.创建Buffer缓冲区,并存入数据
String data = "hello, 跟一一哥学Java吧!";
ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
//3.将缓冲区里的数据写入到文件中
channel.write(buffer);
//4.关闭资源
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上面的案例中,首先我们获取了一个WritableByteChannel的子类对象FileChannel,该对象就可以写入数据到一个文件中。我们在try代码块中获取到了FileChannel实例,并指定文件的打开选项,其中WRITE表示可写入数据,CREATE表示在文件不存在的情况下创建新文件。
接下来我们创建了一个ByteBuffer来存储要写入的数据。在这个例子中,壹哥使用了wrap方法将一个字节数组包装成一个ByteBuffer对象。
然后,我们又将ByteBuffer中的数据写入到了Channel中。write方法会将ByteBuffer中的数据写入到Channel中,返回值表示实际写入的字节数。在这个例子中我们写入的是一个字符串,所以返回值应该是字符串的字节数。
最后,我们把Channel资源关闭释放。
6. 注意事项
我们在使用Channel和Buffer读取和写入数据时,需要注意以下几点:
调用write方法之前,需要确保ByteBuffer中包含了要写入的数据; 写入数据之前,需要调用clear方法清空ByteBuffer,以确保它的位置和限制都设置正确; Channel的实现类通常会使用缓存,所以在写入数据后需要调用force方法,将缓存中的数据刷新到磁盘中; 在完成所有写入操作后需要关闭Channel。
除了使用Channel和Buffer写入数据,NIO还提供了其他写入数据的方式,比如使用FileChannel的transferTo和transferFrom方法,以及使用SocketChannel和ServerSocketChannel等网络编程相关的类。这些内容壹哥会开辟NIO专栏进行讲解,欢迎大家持续关注壹哥哦。
------------------------------正片已结束,来根事后烟----------------------------
二. 结语
NIO模型相比BIO有了很大的改进提升,其重点内容如下:
- NIO模型是非阻塞式的IO模型,数据的读取和写入都是非阻塞的;
- NIO模型中,一个线程可以同时处理多个客户端连接的I/O操作,不需要为每个客户端的连接都创建一个线程;
- NIO模型大大降低了线程的创建和切换开销,从而提高了服务器的并发性能;
- NIO模式有三个核心组件,包括Channel(通道)、Buffer(缓冲区)和Selector(选择器)。
在下一篇文章中,壹哥会给大家介绍AIO异步非阻塞式模型,敬请大家继续关注。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。