Java nio 包,可以理解为( new io )。
标准io的缺点:
(1) 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间
(2) 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。
nio 提供了和标准 io 不同的工作方式:
(1) 采用 Buffer 和 Channel .
标准的IO基于字节流和字符流进行操作的,即 Byte + Stream(面向流).而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中(面向缓冲区)。
(2) 使用异步I/O.
Java I/O是阻塞式的,所以服务器也采用阻塞式I/O进行数据的读写操作。
但 Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
(3) 利用了selector(选择器).
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
nio可以实现利用单线程来管理多个通道,但是付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
用图来表示如下:
Java nio 最核心的东西是 Buffer 、 Channel 和 Selector. 接下来我们先分开来就讲这三部分。
1 . Channel(通道) :
以下是 Java nio 包含的主要 Channel 实现:
FileChannel: 从文件中读写数据。
DatagramChannel:能通过UDP读写网络中的数据。
SocketChannel: 能通过TCP读写网络中的数据。
ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
2 . Buffer(缓冲区):
以下是 Java nio 包含的主要 Buffer 实现( 7种基本数据类型 ):
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
整个Buffer的处理过程:
(1) 写入数据到 Buffer(allocate方法分配+写入数据)。
(2) 利用 buf.flip() 反转 Buffer 的读写状态(从写模式切换到读模式)
(3) 从Buffer中读取数据。
(4) 调用clear()方法或者compact()方法,compact()只清除已经读过的数据,clear()是清除所有数据。
Buffer 有三个属性: capacity position limit
解析:
capacity : 作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.
posotion : 当你写数据到Buffer中时,position表示当前的位置。初始的
position值为0.当一个byte、long等数据写到Buffer后, position
会向前移动到下一个可插入数据的Buffer单元。position最大可为
capacity – 1。
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读
模式,position会被重置为0。当从Buffer的position处读取数据
limit : 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写
模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,
当切换Buffer到读模式时,limit会被设置成写模式下的position值。
换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数
量,这个值在写模式下就是position)
关于分散(Scatter)/聚集(Gather) :
(1)用于描述从Channel中读取或者写入到Channel的操作。
(2)分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。
(3)聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,
3 . Selector(选择器) :
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
Selector的处理过程:
(1) 通过调用Selector.open()方法创建一个Selector
(2) 通过SelectableChannel.register()方法来实现将channel注册到selector上.
(3) register方法会返回一个 SelectionKey对象,可以通过SelectionKey的selectedKeySet()方法访问这些对象。
(4) 一旦将channel注册到selector上,就可以调用select()返回已经准备就绪的通道。
(5)用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
Selector监听Channel的时候,可以监听四种不同类型的事件:
Connect : 连接就绪--> 客户端连接服务端事件
Accept : 接收就绪--> 服务端接收客户端连接事件
Read : 读就绪---> 一个有数据可读的通道
Write : 写就绪----> 等待写数据的通道
4.管道连接:
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
处理过程如下:
(1)通过Pipe.open()方法打开管道
(2)要向管道写数据,需要访问sink通道
(3)通过调用SinkChannel的write()方法,将数据写入SinkChannel
(4)从读取管道的数据,需要访问source通道
(5)调用source通道的read()方法来读取数据
(6)read()方法返回的int值会告诉我们多少字节被读进了缓冲区。
5.I/O模型:
我们先来区别同步、异步、阻塞、非阻塞的概念。
同步:多个任务要执行的时候,必须按顺序一个一个执行。其中一个任务执行的过程中,别的任务都要等待。
异步:多个任务都要执行的时候,可以并发执行。
阻塞:一个任务执行的过程中,它所需要的执行条件如果不满足,则需要等待条件满足后再继续执行。
非阻塞:一个任务在执行的过程中,它需要的条件不满足,不等待,只返回一个条件不满足的信息给它。
同步和异步强调的是其他线程是否需要等待,而阻塞和非阻塞强调自身是否需要等待。
接下来,我们区分阻塞I/O和非阻塞I/O的概念。
通常来说,IO操作包括:对硬盘的读写、对socket的读写以及外设的读写。
(1)当用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪。
(2)对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;
(3)对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。线程本身不会等待。
(4)数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作。
接下来,我们区分同步I/O和异步I/O的概念。
同步I/O:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;
而异步IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。
同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。
是否阻塞和同步异步I/O的微小区别:
一个完整的IO读请求操作包括两个阶段:
1)查看数据是否就绪;
2)进行数据拷贝(内核将数据拷贝到用户线程)。
是否阻塞描述的是在第一阶段查看数据是否就绪时,线程的不同处理方式。
同步和异步I/O描述的是在第二阶段数据是由用户线程完成还是由内核完成。
五种I/O模型:
阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
1.阻塞IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
内核负责查看需要的数据是否准备就绪。阻塞状态下,用户线程会交出cpu.
2.非阻塞IO模型
在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻
3.多路复用IO模型
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。
在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
4.信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
5.异步IO模型
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
4和5的区别在于:数据拷贝的过程是由用户线程自己完成还是由内核完成。
也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
最后简单介绍两种高性能的I/O设计模式:Reactor和Proactor。
Reactor模式适用于同步操作,Proactor适用于异步操作。
注意:在单核的机上,多线程并不能提高系统的性能,线程切换的开销会使处理的速度变慢。但当有阻碍操作发生时,多线程的优势才会显示出来。