Java NIO学习一
来源:互联网 发布:网络电视怎么看电视台 编辑:程序博客网 时间:2024/05/16 11:53
一、NIO概述
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
举例来说,传统BIO的处理方式大概如下:
{ ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池 ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(8088); while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来 Socket socket = serverSocket.accept(); executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程}class ConnectIOnHandler extends Thread{ private Socket socket; public ConnectIOnHandler(Socket socket){ this.socket = socket; } public void run(){ while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件 String someThing = socket.read()....//读取数据 if(someThing!=null){ ......//处理数据 socket.write()....//写数据 } } }}
这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。
现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:
- 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
- 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
- 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
- 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。
二、NIO的实现原理
回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,我们无法知道到底能不能写、能不能读,因为这是个阻塞式操作,只有数据流到的时候才能执行,只能”傻等”。即使通过各种估算,算出来当前操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以为了提高CPU利用率,只能使用多线程。
对NIO来说,NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接条件还未满足,该连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这个件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。这其实就是通过事件驱动模型来进行管理的。
下面具体看下如何利用事件模型单线程处理所有I/O请求:
我们首先需要注册相应的处理器来处理这几个事件的到来。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:
我们用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示是那个事件的到来。
注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。
拿服务器和客户端的处理模型来说,服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。
总的来看,java NIO的基本处理原则如下:
1. 通过一个专门的线程来处理所有的 IO 事件,并负责分发。
2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
三、缓冲区Buffer
Buffer是一个对象,它包含一些要写入或读出的数据。在NIO中,数据是放入buffer对象的,而在IO中,数据是直接写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。
在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这里先不进行陈述。
使用 Buffer 读写数据一般遵循以下四个步骤:
- 分配空间(ByteBuffer buf = ByteBuffer.allocate(1024); )
- 写入数据到 Buffer;(int bytesRead = fileChannel.read(buf);)
- 调用 flip() 方法;( buf.flip();)
- 从 Buffer 中读取数据;(System.out.print((char)buf.get());)
- 调用 clear() 方法或者 compact() 方法。
向Buffer中写数据:
- 从Channel写到Buffer (fileChannel.read(buf))
- 通过Buffer的put()方法 (buf.put(…))
从Buffer中读取数据:
- 从Buffer读取到Channel (channel.write(buf))
- 使用get()方法从Buffer中读取数据 (buf.get())
当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态:capacity, position, limit, mark:
举例来说,我们通过ByteBuffer.allocate(11)方法创建了一个11个byte的数组的缓冲区,初始状态如下图,position的位置为0,capacity和limit默认都是数组长度。
当我们写入5个字节时,变化如下图:
这时我们需要将缓冲区中的5个字节数据写入Channel的通信信道,所以我们调用ByteBuffer.flip()方法,变化如下图所示(position设回0,并将limit设成之前的position的值):
这时底层操作系统就可以从缓冲区中正确读取这个5个字节数据并发送出去了。在下一次写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。
调用clear()方法时,position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。
compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定的position,之后可以通过调用Buffer.reset()方法恢复到这个position。Buffer.rewind()方法将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。
四、通道Channel
Channel是一个对象,可以通过它读取和写入数据。可以把它看做IO中的流。但是它和流相比还有一些不同:
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过buffer对象
正如上面提到的,所有数据都通过Buffer对象处理,所以,您永远不会将字节直接写入到Channel中,相反,您是将数据写入到Buffer中;同样,您也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。
因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。特别是在Unix模型中,底层操作系统通常都是双向的。
在Java NIO中Channel主要有如下几种类型:
- FileChannel:从文件读取数据的
- DatagramChannel:读写UDP网络协议数据
- SocketChannel:读写TCP网络协议数据
- ServerSocketChannel:可以监听TCP连接
打开通道比较简单,除了FileChannel,都用open方法打开。
下面来看一下通道间数据传输的代码:
private static void channelCopy(ReadableByteChannel src, WritableByteChannel dest) throws IOException { ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024); while (src.read(buffer) != -1) {//从通道中读取数据 // 切换读写模式 buffer.flip(); // 向通道中写入数据 dest.write(buffer); buffer.compact(); } //循环退出后可能还有数据未处理,完成最后一次写入 buffer.flip(); while (buffer.hasRemaining()) { dest.write(buffer); }}
通道不能被重复使用,这点与缓冲区不同;关闭通道后,通道将不再连接任何东西,任何的读或写操作都会导致ClosedChannelException。
五、示例演示
NIO中从通道中读取:创建一个缓冲区,然后让通道读取数据到缓冲区。NIO写入数据到通道:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入。
1、从文件中读取
我们已经知道,在NIO系统中,任何时候执行一个读操作,都是从Channel中读取,但又不是直接从Channel中读取数据,因为所有的数据都必须用Buffer来封装,所以应该是从Channel读取数据到Buffer。因此,如果从文件读取数据的话,需要如下步骤:
- 第一步:获取通道
FileInputStream fin = new FileInputStream( "test.txt" );FileChannel fc = fin.getChannel();
- 第二步:创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
- 第三步:将数据从通道读到缓冲区
fc.read( buffer );
示例:
package com.kang;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;public class ReadFileApp { public static void readFileByIO(String fileName) { FileInputStream fis = null; try { fis = new FileInputStream(fileName); byte[] buffer = new byte[1024]; int len = 0; while ((len = fis.read(buffer)) != -1) { System.out.write(buffer, 0, len); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { fis.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String[] args) { String fileName = "E:\\testFile.txt"; ReadFileApp.readFileByIO(fileName); }}
运行结果:
2、写入数据到文件
- 第一步:获取一个通道
FileOutputStream fout = new FileOutputStream( "newtest.txt" );FileChannel fc = fout.getChannel();
- 第二步:创建缓冲区,将数据放入缓冲区
ByteBuffer buffer = ByteBuffer.allocate( 1024 );for (int i=0; i<message.length; ++i) { buffer.put( message[i] );}buffer.flip();
- 第三步:把缓冲区数据写入通道中
fc.write( buffer );
示例:
package com.kang;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class WriteFileApp { public static void writeFileByNIO(String file) { FileOutputStream fos = null; FileChannel fc = null; ByteBuffer buffer = null; try { fos = new FileOutputStream(file); // 第一步 获取一个通道 fc = fos.getChannel(); // buffer=ByteBuffer.allocate(1024); // 第二步 定义缓冲区 buffer = ByteBuffer.wrap("Hello World".getBytes()); // 将内容写到缓冲区 fos.flush(); fc.write(buffer); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { fc.close(); fos.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static void main(String[] args) { String file = "E:\\hello.txt"; WriteFileApp.writeFileByNIO(file); }}
3、读写结合
CopyFile是一个非常好的读写结合的例子。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。
package com.kang;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel;public class CopyFileApp { public static void copyFileUseNIO(String src, String dst) throws IOException { // 声明源文件和目标文件 FileInputStream fi = new FileInputStream(new File(src)); FileOutputStream fo = new FileOutputStream(new File(dst)); // 获得传输通道channel FileChannel inChannel = fi.getChannel(); FileChannel outChannel = fo.getChannel(); // 获得容器buffer ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { // 判断是否读完文件 int eof = inChannel.read(buffer); if (eof == -1) { break; } // 重设一下buffer的position=0,limit=position buffer.flip(); // 开始写 outChannel.write(buffer); // 写完要重置buffer,重设position=0,limit=capacity buffer.clear(); } inChannel.close(); outChannel.close(); fi.close(); fo.close(); } public static void main(String[] args) { String src="E:\\testFile.txt"; String dst="E:\\new_testFile.txt"; try { CopyFileApp.copyFileUseNIO(src, dst); } catch (IOException e) { e.printStackTrace(); } }}
- Java NIO学习一
- NIO学习(一) java nio介绍
- Java NIO学习(一)
- Java NIO 学习(一)
- java nio学习(一)
- Java NIO 学习(一)--简介
- Java NIO学习(一)
- java nio学习(一)
- java学习-NIO(一)简介
- 学习 java netty (一) -- java nio
- Java NIO学习(一)NIO相关概念
- Java NIO学习笔记一(IO VS NIO)
- Java NIO入门学习(一)
- NIO学习一、NIO简介
- Java NIO:一、NIO基础
- NIO学习笔记(一)
- nio基础学习(一)
- NIO学习笔记一
- phpstudy You don't have permission to access /phpinfo.php on this server.
- 查找算法学习(4)
- linux在线安装git方法
- 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数
- POJ 1321 棋盘问题
- Java NIO学习一
- Oracle 在AIX上的性能调整 -- 内存篇
- 排序一个列表序列,并统计每一个元素出现的次数
- 分享自己折腾多时的一套 vue 组件 -- we-vue
- spring之注解(三)Component
- 【Redis基础】持久化机制
- 平衡二叉树详解
- c++ stream关系
- java 实现注册时发送激活邮件+激活