Java中的IO、NIO、AIO

1、IO流(同步、阻塞)

一、概述

IO流简单来讲就是input和output流,IO流主要是用来处理设备之间的数据传输,Java IO对于数据的操做都是经过流实现的,而java用于操做流的对象都在IO包中。java

二、分类

按操做数据分为:字节流(Reader、Writer)和字符流(InputStream和OutputStream)编程

按流向分为:输入流(Reader、InputStream)和输出流(Writer、OutputStream)数组

 

 

 

 

三、字符流

概述

只用来处理文本数据网络

数据最多见的表现形式是文件,字符流用来操做文件的子类通常是FileReader和FileWriter多线程

字符流读写文件注意事项:并发

  • 写入文件必需要用flush()刷新
  • 用完流记得要关闭流
  • 使用流对象要抛出IO异常
  • 定义文件路径时,能够用"/"或者"\"
  • 在建立一个文件时,若是目录下有同名文件将被覆盖
  • 在读取文件时,必须保证该文件已存在,不然抛出异常

字符流的缓冲区

  • 缓冲区的出现是为了提升流的操做效率而出现的
  • 须要被提升效率的流做为参数传递给缓冲区的构造函数
  • 在缓冲区中封装了一个数组,存入数据后一次取出

四、字节流

概述

用来处理媒体数据异步

字节流读写文件注意事项:socket

  • 字节流和字符流的基本操做是相同的,可是想要操做媒体流就须要用到字节流
  • 字节流由于操做的是字节,因此能够用来操做媒体文件(媒体文件也是以字节存储的)
  • 输入流(InputStream)、输出流(OutputStream)
  • 字节流操做能够不用刷新流操做
  • InputStream特有方法:int available()(返回文件中的字节个数)

字节流的缓冲区
字节流缓冲区跟字符流缓冲区同样,也是为了提升效率函数

 

2、NIO(同步、非阻塞)

NIO之因此是同步,是由于它的accept/read/write方法的内核I/O操做都会阻塞当前线程高并发

首先,咱们要先了解一下NIO的三个主要组成部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

(1)Channel(通道)

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链接

(2)Buffer

Buffer是一个对象,它包含一些要写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操做,而必须经过 Buffer 来进行,即 Channel 是经过 Buffer 来读写数据的。

在NIO中,全部的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,一般是一个字节数据,但也能够是其余类型的数组。但一个缓冲区不只仅是一个数组,重要的是它提供了对数据的结构化访问,并且还能够跟踪系统的读写进程。

使用 Buffer 读写数据通常遵循如下四个步骤:

1.写入数据到 Buffer;

2.调用 flip() 方法;

3.从 Buffer 中读取数据;

4.调用 clear() 方法或者 compact() 方法。

当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,须要经过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,能够读取以前写入到 Buffer 的全部数据。

一旦读完了全部的数据,就须要清空缓冲区,让它能够再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

Buffer主要有以下几种:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

copyFile实例(NIO)

CopyFile是一个很是好的读写结合的例子,咱们将经过CopyFile这个实力让你们体会NIO的操做过程。CopyFile执行三个基本的操做:建立一个Buffer,而后从源文件读取数据到缓冲区,而后再将缓冲区写入目标文件。

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(); } 

(三)Selector(选择器对象)

首先须要了解一件事情就是线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。

Selector是一个对象,它能够注册到不少个Channel上,监听各个Channel上发生的事件,而且可以根据事件状况决定Channel读写。这样,经过一个线程管理多个Channel,就能够处理大量网络链接了。

selector优势

有了Selector,咱们就能够利用一个线程来处理全部的channels。线程之间的切换对操做系统来讲代价是很高的,而且每一个线程也会占用必定的系统资源。因此,对系统来讲使用的线程越少越好。

1.如何建立一个Selector

Selector 就是您注册对各类 I/O 事件兴趣的地方,并且当那些事件发生时,就是这个对象告诉您所发生的事件。

Selector selector = Selector.open();

 

2.注册Channel到Selector

为了能让Channel和Selector配合使用,咱们须要把Channel注册到Selector上。经过调用 channel.register()方法来实现注册:

channel.configureBlocking(false); SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

 

注意,注册的Channel 必须设置成异步模式 才能够,不然异步IO就没法工做,这就意味着咱们不能把一个FileChannel注册到Selector,由于FileChannel没有异步模式,可是网络编程中的SocketChannel是能够的。

3.关于SelectionKey

请注意对register()的调用的返回值是一个SelectionKey。 SelectionKey 表明这个通道在此 Selector 上注册。当某个 Selector 通知您某个传入事件时,它是经过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还能够用于取消通道的注册。

SelectionKey中包含以下属性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

(1)Interest set

就像咱们在前面讲到的把Channel注册到Selector来监听感兴趣的事件,interest set就是你要选择的感兴趣的事件的集合。你能够经过SelectionKey对象来读写interest set:

int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

 

经过上面例子能够看到,咱们能够经过用AND 和SelectionKey 中的常量作运算,从SelectionKey中找到咱们感兴趣的事件。

(2)Ready Set

ready set 是通道已经准备就绪的操做的集合。在一次选Selection以后,你应该会首先访问这个ready set。Selection将在下一小节进行解释。能够这样访问ready集合:

int readySet = selectionKey.readyOps();

 

能够用像检测interest集合那样的方法,来检测Channel中什么事件或操做已经就绪。可是,也可使用如下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();

 

(3)Channel 和 Selector

咱们能够经过SelectionKey得到Selector和注册的Channel:

Channel  channel  = selectionKey.channel(); Selector selector = selectionKey.selector();

 

(4)Attach一个对象

能够将一个对象或者更多信息attach 到SelectionKey上,这样就能方便的识别某个给定的通道。例如,能够附加 与通道一块儿使用的Buffer,或是包含汇集数据的某个对象。使用方法以下:

selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();

 

还能够在用register()方法向Selector注册Channel的时候附加对象。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

 

4.关于SelectedKeys()

生产系统中通常会额外进行就绪状态检查

一旦调用了select()方法,它就会返回一个数值,表示一个或多个通道已经就绪,而后你就能够经过调用selector.selectedKeys()方法返回的SelectionKey集合来得到就绪的Channel。请看演示方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

 

当你经过Selector注册一个Channel时,channel.register()方法会返回一个SelectionKey对象,这个对象就表明了你注册的Channel。这些对象能够经过selectedKeys()方法得到。你能够经过迭代这些selected key来得到就绪的Channel,下面是演示代码:

Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) { // a connection was established with a remote server.
} else if (key.isReadable()) { // a channel is ready for reading
} else if (key.isWritable()) { // a channel is ready for writing
} keyIterator.remove(); }

 

这个循环遍历selected key的集合中的每一个key,并对每一个key作测试来判断哪一个Channel已经就绪。

请注意循环中最后的keyIterator.remove()方法。Selector对象并不会从本身的selected key集合中自动移除SelectionKey实例。咱们须要在处理完一个Channel的时候本身去移除。当下一次Channel就绪的时候,Selector会再次把它添加到selected key集合中。

SelectionKey.channel()方法返回的Channel须要转换成你具体要处理的类型,好比是ServerSocketChannel或者SocketChannel等等。

 

(4)NIO多路复用

主要步骤和元素:

  • 首先,经过 Selector.open() 建立一个 Selector,做为相似调度员的角色。

  • 而后,建立一个 ServerSocketChannel,而且向 Selector 注册,经过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的链接请求。

  • 注意,为何咱们要明确配置非阻塞模式呢?这是由于阻塞模式下,注册操做是不容许的,会抛出 IllegalBlockingModeException 异常。

  • Selector 阻塞在 select 操做,当有 Channel 发生接入请求,就会被唤醒。

  • 在 具体的 方法中,经过 SocketChannel 和 Buffer 进行数据操做

IO 都是同步阻塞模式,因此须要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,经过高效地定位就绪的 Channel,来决定作什么,仅仅 select 阶段是阻塞的,能够有效避免大量客户端链接时,频繁线程切换带来的问题,应用的扩展能力有了很是大的提升

 

3、NIO2(异步、非阻塞)

AIO是异步IO的缩写,虽然NIO在网络操做中,提供了非阻塞的方法,可是NIO的IO行为仍是同步的。对于NIO来讲,咱们的业务线程是在IO操做准备好时,获得通知,接着就由这个线程自行进行IO操做,IO操做自己是同步的。

可是对AIO来讲,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操做已经完成后,再给线程发出通知。所以AIO是不会阻塞的,此时咱们的业务逻辑将变成一个回调函数,等待IO操做完成后,由系统自动触发。

与NIO不一样,当进行读写操做时,只须直接调用API的read或write方法便可。这两种方法均为异步的,对于读操做而言,当有流可读取时,操做系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操做而言,当操做系统将write方法传递的流写入完毕时,操做系统主动通知应用程序。 便可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部份内容被称做NIO.2,主要在Java.nio.channels包下增长了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户链接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。

在AIO编程中,发出一个事件(accept read write等)以后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义了以下两个方法,分别在异步操做成功和失败时被回调。

void completed(V result, A attachment); void failed(Throwable exc, A attachment);
相关文章
相关标签/搜索