java基础的IO、NIO、AIO详解

概述
在咱们学习Java的IO流以前,咱们都要了解几个关键词java

同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当咱们进行同步操做时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其余任务不须要等待当前调用返回,一般依靠事件、回调等机制来实现任务间次序关系
阻塞与非阻塞:在进行阻塞操做时,当前线程会处于阻塞状态,没法从事其余任务,只有当条件就绪才能继续,好比ServerSocket新链接创建完毕,或者数据读取、写入操做完成;而非阻塞则是无论IO操做是否结束,直接返回,相应操做在后台继续处理
同步和异步的概念:实际的I/O操做编程

同步是用户线程发起I/O请求后须要等待或者轮询内核I/O操做完成后才能继续执行segmentfault

异步是用户线程发起I/O请求后仍须要继续执行,当内核I/O操做完成后会通知用户线程,或者调用用户线程注册的回调函数数组

阻塞和非阻塞的概念:发起I/O请求网络

阻塞是指I/O操做须要完全完成后才能返回用户空间多线程

非阻塞是指I/O操做被调用后当即返回一个状态值,无需等I/O操做完全完成并发

BIO、NIO、AIO的概述
首先,传统的 java.io包,它基于流模型实现,提供了咱们最熟知的一些 IO 功能,好比 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动做完成以前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。框架

java.io包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。异步

不少时候,人们也把 java.net下面提供的部分网络 API,好比 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,由于网络通讯一样是 IO 行为。socket

第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,能够构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操做系统底层的高性能数据操做方式。

第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有不少人叫它 AIO(Asynchronous IO)。异步 IO 操做基于事件和回调机制,能够简单理解为,应用操做直接返回,而不会阻塞在那里,当后台处理完成,操做系统会通知相应线程进行后续工做。

1、IO流(同步、阻塞)
一、概述
IO流简单来讲就是input和output流,IO流主要是用来处理设备之间的数据传输,Java IO对于数据的操做都是经过流实现的,而java用于操做流的对象都在IO包中。

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

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

三、字符流
概述
只用来处理文本数据

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

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

写入文件必需要用flush()刷新
用完流记得要关闭流
使用流对象要抛出IO异常
定义文件路径时,能够用"/"或者""
在建立一个文件时,若是目录下有同名文件将被覆盖
在读取文件时,必须保证该文件已存在,不然抛出异常
字符流的缓冲区
缓冲区的出现是为了提升流的操做效率而出现的
须要被提升效率的流做为参数传递给缓冲区的构造函数
在缓冲区中封装了一个数组,存入数据后一次取出
四、字节流
概述
用来处理媒体数据

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

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

五、Java Scanner类
Java 5添加了java.util.Scanner类,这是一个用于扫描输入文本的新的实用程序

关于nextInt()、next()、nextLine()的理解
nextInt():只能读取数值,如果格式不对,会抛出java.util.InputMismatchException异常

next():碰见第一个有效字符(非空格,非换行符)时,开始扫描,当碰见第一个分隔符或结束符(空格或换行符)时,结束扫描,获取扫描到的内容

nextLine():能够扫描到一行内容并做为字符串而被捕获到

关于hasNext()、hasNextLine()、hasNextxxx()的理解
就是为了判断输入行中是否还存在xxx的意思

与delimiter()有关的方法
应该是输入内容的分隔符设置,

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

相关文章
相关标签/搜索