3

Java NIO与IO的区别和比较

 1 year ago
source link: https://blog.51cto.com/u_15776483/5688375
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java NIO与IO的区别和比较

推荐 原创

是温度呀 2022-09-19 11:37:03 ©著作权

文章标签 java 数据 非阻塞 后端 程序员 文章分类 Java 编程语言 阅读数240

传统的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号桌要结帐,服务员从前台取一条记录,根据记录提供服务,完了再来取下一条。这样服务的时间就被最有效的利用起来了。

J2SE1.4以上版本中发布了全新的I/O类库。NIO库提供的一些新特性:非阻塞I/O,字符转换,缓冲以及通道。NIO和IO都在rt.jar包中。

一、NIO的简介
NIO包(java.nio.*)引入了四个关键的抽象数据类型,它们共同解决传统的I/O类中的一些问题。
1. Buffer:它是包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
2. Charset:它提供Unicode字符串影射到字节序列以及逆影射的操作。
3. Channels:包含socket,file和pipe三种管道,它实际上是双向交流的通道。
4. Selector:它将多元异步I/O操作集中到一个或多个线程中(它可以被看成是Unix中select()函数或Win32中WaitForSingleEvent()函数的面向对象版本)。

二、IO的传统方式

以网络应用为例,传统方式需要监听一个ServerSocket,接受请求的连接为其提供服务(服务通常包括了处理请求并发送响应)图一是服务器的生命周期图,其中标有粗黑线条的部分表明会发生I/O阻塞。

Java NIO与IO的区别和比较_数据

可以分析创建服务器的每个具体步骤。首先创建ServerSocket
ServerSocket server=new ServerSocket(10000);
然后接受新的连接请求
Socket newCnotallow=server.accept();
对于accept方法的调用将造成阻塞,直到ServerSocket接受到一个连接请求为止。一旦连接请求被接受,服务器可以读客户socket中的请求。

1 InputStream in = newConnection.getInputStream(); 2
3 InputStreamReader reader = new InputStreamReader(in); 4
5 BufferedReader buffer = new BufferedReader(reader); 6
7 Request request = new Request(); 8
9 while(!request.isComplete()) { 10
11 String line = buffer.readLine(); 12
13 request.addLine(line); 14 }

这样的操作有两个问题,首先BufferedReader类的readLine()方法在其缓冲区未满时会造成线程阻塞,只有一定数据填满了缓冲区或者客户关闭了套接字,方法才会返回。其次,它回产生大量的垃圾,BufferedReader创建了缓冲区来从客户套接字读入数据,但是同样创建了一些字符串存储这些数据。虽然BufferedReader内部提供了StringBuffer处理这一问题,但是所有的String很快变成了垃圾需要回收。

同样的问题在发送响应代码中也存在

1 Response response = request.generateResponse(); 2
3 OutputStream out = newConnection.getOutputStream(); 4
5 InputStream in = response.getInputStream(); 6
7 int ch; 8
9 while(-1 != (ch = in.read())) { 10
11 out.write(ch); 12
13 } 14
15 newConnection.close();

类似的,读写操作被阻塞而且向流中一次写入一个字符会造成效率低下,所以应该使用缓冲区,但是一旦使用缓冲,流又会产生更多的垃圾。

传统的解决方法
通常在Java中处理阻塞I/O要用到线程(大量的线程)。一般是实现一个线程池用来处理请求,如图二

Java NIO与IO的区别和比较_程序员_02

图二
线程使得服务器可以处理多个连接,但是它们也同样引发了许多问题。每个线程拥有自己的栈空间并且占用一些CPU时间,耗费很大,而且很多时间是浪费在阻塞的I/O操作上,没有有效的利用CPU。

三、NIO的详细介绍

1.**** Buffer
传统的I/O不断的浪费对象资源(通常是String)。新I/O通过使用Buffer读写数据避免了资源浪费。Buffer对象是线性的,有序的数据集合,它根据其类别只包含唯一的数据类型。
java.nio.Buffer 类描述
java.nio.ByteBuffer 包含字节类型。 可以从ReadableByteChannel中读在 WritableByteChannel中写
java.nio.MappedByteBuffer 包含字节类型,直接在内存某一区域映射
java.nio.CharBuffer 包含字符类型,不能写入通道
java.nio.DoubleBuffer 包含double类型,不能写入通道
java.nio.FloatBuffer 包含float类型
java.nio.IntBuffer 包含int类型
java.nio.LongBuffer 包含long类型
java.nio.ShortBuffer 包含short类型
可以通过调用allocate(int capacity)方法或者allocateDirect(int capacity)方法分配一个Buffer。特别的,你可以创建MappedBytesBuffer通过调用FileChannel.map(int mode,long position,int size)。直接(direct)buffer在内存中分配一段连续的块并使用本地访问方法读写数据。非直接(nondirect)buffer通过使用Java中的数组访问代码读写数据。有时候必须使用非直接缓冲例如使用任何的wrap方法(如ByteBuffer.wrap(byte[]))在Java数组基础上创建buffer。

2. 字符编码
向ByteBuffer中存放数据涉及到两个问题:字节的顺序和字符转换。ByteBuffer内部通过ByteOrder类处理了字节顺序问题,但是并没有处理字符转换。事实上,ByteBuffer没有提供方法读写String。
Java.nio.charset.Charset处理了字符转换问题。它通过构造CharsetEncoder和CharsetDecoder将字符序列转换成字节和逆转换。

3. 通道****(Channel)
你可能注意到现有的java.io类中没有一个能够读写Buffer类型,所以NIO中提供了Channel类来读写Buffer。通道可以认为是一种连接,可以是到特定设备,程序或者是网络的连接。通道的类等级结构图如下

Java NIO与IO的区别和比较_java_03

图三
图中ReadableByteChannel和WritableByteChannel分别用于读写。
GatheringByteChannel可以从使用一次将多个Buffer中的数据写入通道,相反的,ScatteringByteChannel则可以一次将数据从通道读入多个Buffer中。你还可以设置通道使其为阻塞或非阻塞I/O操作服务。
为了使通道能够同传统I/O类相容,Channel类提供了静态方法创建Stream或Reader

4.**** Selector
在过去的阻塞I/O中,我们一般知道什么时候可以向stream中读或写,因为方法调用直到stream准备好时返回。但是使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了。在NIO包中,设计Selector就是为了这个目的。SelectableChannel可以注册特定的事件,而不是在事件发生时通知应用,通道跟踪事件。然后,当应用调用Selector上的任意一个selection方法时,它查看注册了的通道看是否有任何感兴趣的事件发生。图四是selector和两个已注册的通道的例子

Java NIO与IO的区别和比较_java_04

图四
并不是所有的通道都支持所有的操作。SelectionKey类定义了所有可能的操作位,将要用两次。首先,当应用调用SelectableChannel.register(Selector sel,int op)方法注册通道时,它将所需操作作为第二个参数传递到方法中。然后,一旦SelectionKey被选中了,SelectionKey的readyOps()方法返回所有通道支持操作的数位的和。SelectableChannel的validOps方法返回每个通道允许的操作。注册通道不支持的操作将引发IllegalArgumentException异常。下表列出了SelectableChannel子类所支持的操作。

1 ServerSocketChannel OP_ACCEPT 2
3 SocketChannel OP_CONNECT, OP_READ, OP_WRITE 4
5 DatagramChannel OP_READ, OP_WRITE 6
7 Pipe.SourceChannel OP_READ 8
9 Pipe.SinkChannel OP_WRITE

四. 举例说明

1. 简单网页内容下载
这个例子非常简单,类SocketChannelReader使用SocketChannel来下载特定网页的HTML内容。
package examples.nio;

package com.yineng.mycat; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.net.InetSocketAddress; import java.io.IOException; /** * @author kzfy
* @data 2015/12/21 */
public class SocketChannelReader{ private Charset charset=Charset.forName("UTF-8");//创建UTF-8字符集

private SocketChannel channel; public void getHTMLContent(){ try{
connect();
sendRequest();
readResponse();
}catch(IOException e){
System.err.println(e.toString());
}finally{ if(channel!=null){ try{
channel.close();
}catch(IOException e){}
}
}
} private void connect()throws IOException{//连接到CSDN InetSocketAddress socketAddress= new InetSocketAddress("http://www.csdn.net",80);
channel=SocketChannel.open(socketAddress); //使用工厂方法open创建一个channel并将它连接到指定地址上 //相当与SocketChannel.open().connect(socketAddress);调用 } private void sendRequest()throws IOException{
channel.write(charset.encode("GET /document\r\n\r\n"));//发送GET请求到CSDN的文档中心 //使用channel.write方法,它需要CharByte类型的参数,使用 //Charset.encode(String)方法转换字符串。
} private void readResponse()throws IOException{//读取应答
ByteBuffer buffer=ByteBuffer.allocate(1024);//创建1024字节的缓冲
while(channel.read(buffer)!=-1){
buffer.flip();//flip方法在读缓冲区字节操作之前调用。
System.out.println(charset.decode(buffer)); //使用Charset.decode方法将字节转换为字符串
buffer.clear();//清空缓冲
}
} public static void main(String [] args){ new SocketChannelReader().getHTMLContent();
}
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK