传统IO与NIO的比较

xiaoxiao2021-02-28  10

  传统的socket IO中,需要为每个连接创建一个线程,当并发的连接数量非常巨大时,线程所占用的栈内存和CPU线程切换的开销将非常巨大。使用NIO,不再需要为每个线程创建单独的线程,可以用一个含有限数量线程的线程池,甚至一个线程来为任意数量的连接服务。由于线程数量小于连接数量,所以每个线程进行IO操作时就不能阻塞,如果阻塞的话,有些连接就得不到处理,NIO提供了这种非阻塞的能力。

 

小量的线程如何同时为大量连接服务呢,答案就是就绪选择。这就好比到餐厅吃饭,每来一桌客人,都有一个服务员专门为你服务,从你到餐厅到结帐走人,这样方式的好处是服务质量好,一对一的服务,VIP啊,可是缺点也很明显,成本高,如果餐厅生意好,同时来100桌客人,就需要100个服务员,那老板发工资的时候得心痛死了,这就是传统的一个连接一个线程的方式。

 

老板是什么人啊,精着呢。这老板就得捉摸怎么能用10个服务员同时为100桌客人服务呢,老板就发现,服务员在为客人服务的过程中并不是一直都忙着,客人点完菜,上完菜,吃着的这段时间,服务员就闲下来了,可是这个服务员还是被这桌客人占用着,不能为别的客人服务,用华为领导的话说,就是工作不饱满。那怎么把这段闲着的时间利用起来呢。这餐厅老板就想了一个办法,让一个服务员(前台)专门负责收集客人的需求,登记下来,比如有客人进来了、客人点菜了,客人要结帐了,都先记录下来按顺序排好。每个服务员到这里领一个需求,比如点菜,就拿着菜单帮客人点菜去了。点好菜以后,服务员马上回来,领取下一个需求,继续为别人客人服务去了。这种方式服务质量就不如一对一的服务了,当客人数据很多的时候可能需要等待。但好处也很明显,由于在客人正吃饭着的时候服务员不用闲着了,服务员这个时间内可以为其他客人服务了,原来10个服务员最多同时为10桌客人服务,现在可能为50桌,60客人服务了。

 

这种服务方式跟传统的区别有两个:

1、增加了一个角色,要有一个专门负责收集客人需求的人。NIO里对应的就是Selector。

2、由阻塞服务方式改为非阻塞服务了,客人吃着的时候服务员不用一直侯在客人旁边了。传统的IO操作,比如read(),当没有数据可读的时候,线程一直阻塞被占用,直到数据到来。NIO中没有数据可读时,read()会立即返回0,线程不会阻塞。

 

NIO中,客户端创建一个连接后,先要将连接注册到Selector,相当于客人进入餐厅后,告诉前台你要用餐,前台会告诉你你的桌号是几号,然后你就可能到那张桌子坐下了,SelectionKey就是桌号。当某一桌需要服务时,前台就记录哪一桌需要什么服务,比如1号桌要点菜,2号桌要结帐,服务员从前台取一条记录,根据记录提供服务,完了再来取下一条。这样服务的时间就被最有效的利用起来了。

本文并非Java.io或Java.nio的使用手册,也不是如何使用Java.io与Java.nio的技术文档。这里只是尝试比较这两个包,用最简单的方式突出它们的区别和各自的特性。Java.nio提出了新的流(stream)通讯概念并且加入了新的缓冲、文件流以及socket(套接字)特性。

java.io 概览

这个包通过数据流和序列化机制来实现系统输入和输出。并且支持多种类型的数据流,包括简单的字节、原生数据类型、地区字符以及对象。流是一个数据的序列:一个程序使用输入流从一个源头读取数据;

另一个程序使用输出流写入并发送数据到目的地。

这些程序都使用字节流来执行字节的输入和输出。所有涉及字节流的类都是继承自InputStream和OutputStream。

关于 InputStream 和 OutputStream

执行InputStream和OutputStream的操作一般都意味着不断循环的将字节逐一从输入流读出或写入到输出流。你可以使用缓冲I/O流来降低I/O成本(凡是I/O请求都经常触发磁盘访问、网络动作或其他一些成本昂贵的操作)。缓冲输入流则是从缓冲的内存区域读取数据,只有缓冲读完才会调用native input API(不同操作系统提供的本地输入流API——译者注)。同样的,缓冲输出流也将数据写入缓冲,只有缓冲写满才会调用native output API。这些带缓冲的API很好的封装了未缓冲的流操作: BufferedInputStream 和 BufferedOutputStream.

File I/O

上面一节主要是针对数据流,它提供一种数据读取和写入的简单模型。真实的数据流其实是涉及种类繁多的数据源和目的地,包括磁盘文件。但是,数据流并不支持所有磁盘文件操作。下面的链接介绍了非数据流的文件I/O:

File 类可以编写平台无关的检查和处理文件、目录的代码。 Random access files支持非序列化的磁盘文件数据访问。

java.net socket

两个在网络上运行的程序之间会建立双向通讯的链接,socket就是其中一个端点。Socket相关的类代表着客户端程序和服务端程序之间的连接。java.NET包提供了两个类:Socket和ServerSocket。它们分别实现了连接的客户端和服务端。

客户端知道服务端运行机器的域名,以及服务器监听的端口,它尝试连接到服务器,如果一切正常,服务器接受并建立连接。当接受连接时,服务器在监听端口上绑定一个新的socket,并且通知远程端点设置客户端的地址和端口。之所以要建立一个新的socket是为了处理已连接客户端请求的同时还能继续监听原始socket上的连接请求。

服务器使用阻塞模式等待客户端连接:serverSocket.accept()是一个阻塞指令,当服务器等待接受连接时主线程不能做任何其他操作。由于这个原因,服务器想要达到多任务处理就只能通过实现一个多线程服务器:每当新建一个socket时就必须为它创建一个新线程。

NIO API

I/O性能经常是一个现代应用的痛处。操作系统持续优化改进I/O性能,JVM也提供了一套运行环境帮助Java程序员规避了绝大多数操作系统I/O之间的差异。这些都让I/O相关编码更加高效和简单,但是却隐藏了操作系统的功能特性。想要增强I/O性能,其实你可以通过一些特殊的编码直接访问操作系统的底层功能,但是这并不是最佳解决方案——你的代码将必须依赖某个操作系统。 Java.nio包应运而生来解决这个难题,它提供了高性能的I/O特性,并支持当今大多数常用的商用操作系统。

JDK1.4的NIO包介绍了一系列新的I/O操作的抽象概念。

java.nio 概览

Java.nio这个新增的包实现了Java平台新的 I/O API。NIO API 包含如下特性:

原生类型数据缓冲 字符集的编码器和解码器 基于Perl风格正则表达式的模式匹配 通道(Channel),一种新的原生I/O抽象概念 支持锁和内存映射的文件接口 通过多路复用、非阻塞的I/O能力实现可伸缩的服务器架构

在SUN(现Oracle)的站点上可以找到java.nio的详细技术文档。这里我将解释一些nio的概念,并且和老的java.io库做下比较。建议不要把java.nio当作java.io的替代品,即使它是java.io的“扩展”。Nio的诞生导致了整个I/O类和接口的重新修订(详情请看)。 NIO中一个最重要的概念是在非阻塞模式下运行,与传统Java I/O类库完全不同。什么是非阻塞模式?

Non blocking mode 非阻塞模式

一个I/O流的字节必须序列化的访问。各种设备,打印机端口、网络连接等都是常见的例子。 数据流通常比阻塞式设备慢,而且经常断断续续。大多数操作系统允许将数据流设置为非阻塞模式,允许进程检查是否流上是否有可用数据,即使没有也不会导致进程阻塞。这种机制能让进程在输入流空闲等待时执行其他逻辑,但是数据到达时又能及时处理。操作系统能够观察一堆的数据流并且指示其中哪些处于可用状态。通过观察这些可用状态,一个进程可以采用常用的编码和单个线程就能同时处理多个活动的数据流。这在网络服务器处理大量网络连接时被广泛使用。

Buffers 缓冲

从简单的入手,首先的改进是一系列java.io包中新建的缓冲类。这些缓冲提供了一个可以在内存容器中存储一堆原生类型数据的机制。一个缓冲对象是一个固定容量的容器,容器中的数据可以被读写。

所有的缓冲都是可读的,但并非都是可写的。每个缓冲类都实现了isReadOnly()方法来表示缓冲内容是否允许被修改。

Channels

缓冲需要配合Channel来使用。Channel是I/O传输的入口,缓冲则是数据传输的源头或目的地。在写入时,你想发送的数据首先被放入缓冲,接着被传递至一个Channel;在读取时,Channel负责接收数据,然后寄存至你提供的缓冲中。(比如在网络传输时的过程:用户数据——>发送端缓冲——>发送端Channel——>网络传输——>接收端Channel——>接收端缓冲——>用户数据,译者注)

Channel就像一个管道一样,将数据在管道两端的字节缓冲之间进行高效率的传输。它就像是一个网关,通过它可以用最小的成本来访问操作系统本地的I/O服务,而缓冲则是在两端内部的端点,Channel使用它来发送和接收数据。

Channel能在非阻塞模式下运行。一个非阻塞模式下的Channel不会让调用线程睡眠。请求的操作要么立刻完成、要么返回一个结果告知什么都没做。只有面向数据流的Channel,比如socket,能够在非阻塞模式下运行。在java.nio中的Channel包括FileChannel、ServerSocketChannel以及SocketChannel;这些都是为文件和socket管理所提供的特定的Channel。

FileChannel

FileChannel是可读可写的Channel,它必须阻塞,不能用在非阻塞模式中。面向数据流I/O的非阻塞风格并不适合面向文件的操作,因为文件I/O有本质上的区别。

FileChannel对象不能被直接创建。一个FileChannel实例只能通过在打开的文件对象(RandomAccessFile、FileInputStream、或FileOutputStream)上调用getChannel()得到。GetChannel()方法返回一个连接到相同文件的FileChannel对象,与文件对象拥有相同的访问权限。FileChannel对象是线程安全的。多线程能够并发的调用同一个实例上的方法而不会导致任何问题,但是并非所有操作是支持多线程的。影响Channel位置或者文件大小的操作必须是单线程的。

使用FileChannel,可以让拷贝等操作变成Channel到Channel的传输(transferTo()和transferFrom()方法),而且读写操作更易于使用缓冲。

SocketChannel

SocketChannel与FileChannel不同:新的Socket Channel能在非阻塞模式下运行并且是可选择的。不再需要为每个socket连接指派线程了。使用新的NIO类,一个或多个线程能管理成百上千个活动的socket连接,只用到很小甚至0的性能损失。使用Selector对象可以选择可用的Socket Channel。

有3个Socket Channel类型:SocketChannel, ServerSocketChannel, 以及 DatagramChannel; SocketChannel和DatagramChannel是可读可写的,ServerSocketChannel 监听到来的连接,并且创建新的SocketChannel 对象。所有这些SocketChannel初始化时会创建一个同等的socket对象(java.Netsockets)。这个同等的socket能从Channel对象上调用socket()方法获取。每个Socket Channel (in java.nio.channels)对象拥有一个相关的java.net.socket对象,反之并非所有的socket对象拥有相关的Channel对象。如果你使用传统方式创建一个socket对象,直接初始化它,它将不会拥有一个相关的Channel对象,它的getChannel()方法会返回null。

Socket Channel能在非阻塞模式下运行。传统Java Socket阻塞的本性曾经是影响Java应用可伸缩性的罪魁祸首之一。而基于非阻塞I/O则构建了很多复杂巧妙的、高性能的应用。设置或重置Channel的阻塞模式很简单,只要调用configureBlocking()即可。

非阻塞socket常在服务器端使用因为它能让同时管理多个socket变得简单。

Selector 选择器

选择器(Selector)提供了挑选可用状态Channel的能力,从而实现多路复用的I/O。我下面的例子将很好的解释选择器的优势:

设想你在一个火车站(non-selector,非选择器场景),有3个平台 (channels), 每个平台都有火车到达(buffer,缓冲)。每个平台上都有个管理员管理着到达的列车(worker thread,工作线程)。这是非选择器的场景。现在设想下选择器场景。有3个平台(channels),每个平台会有火车到达(缓冲),每个平台都有个指示器(比如一个响铃)指示着“火车到达”(selection key)。在这个场景下只有一个管理员就可以管理所有的3个平台。他只需查看指示器(selector.select())来判断是否有火车到达然后去处理一下到达的列车。

理解selector场景的优势很简单:一个单线程就可以实现多重任务处理的应用。除此之外,使用非阻塞选择器还能得到更多好处!设想火车管理员看着指示器时:他可以等待新的列车到达而不做其他事(使用selector.select()的阻塞模式);也可以在等待新的列车到达时(非阻塞模式使用selector.selectNow())去卖车票,这样selector就会返回null让线程执行其他代码逻辑。

IO vs. NIO

NIO使得I/O比传统I/O更加高效。在一个I/O操作占很高比例的程序中,想想看会有什么不同。比如,如果一个应用需要使用socket拷贝文件或者传输字节,使用NIO可能会得到更快的性能,因为它比I/O的API更接近操作系统。随着数据量的增大,性能提升将更可观。除了io API里,NIO还为数据流操作提供了其他的功能特性。但是,不可能用NIO取代IO,因为NIO的API是基于java.io之上扩展的功能。NIO通过扩展本地的IO API,为广大开发者使用更加强大的方式操作数据流带来了新的可能性。

转载请注明原文地址: https://www.6miu.com/read-1100034.html

最新回复(0)