您的位置:首页 > 编程语言 > Java开发

Java NIO

2016-03-18 16:40 363 查看
java NIO 和阻塞I/O的区别

阻塞io是面向流的io,服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒 绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。当并发量大,而后端服务或客户端处理数据慢时就会产生产生大量线程处于等待中,即上述的阻塞。



 

无阻塞io是使用单线程或者只使用少量的多线程,每个连接共用一个线程,当处于等待(没有事件)的时候线程资源可以释放出来处理别的请求,通过事件驱动模型当有accept/read/write等事件发生后通知(唤醒)主线程分配资源来处理相关事件。java.nio.channels.Selector就是在该模型中事件的观察者,可以将多个SocketChannel的事件注册到一个Selector上,当没有事件发生时Selector处于阻塞状态,当SocketChannel有accept/read/write等事件发生时唤醒Selector。

 



1. 阻塞I/O通信模型 
我们知道阻塞I/O在调用InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。

阻塞I/O缺点:
1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间
2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。

NIO的工作原理:
1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。 
2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。 
3. 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。

服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

Java NIO的服务端只需启动一个专门的线程来处理所有的 IO 事件,java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:

 
事件名对应值
服务端接收客户端连接事件SelectionKey.OP_ACCEPT(16)
客户端连接服务端事件SelectionKey.OP_CONNECT(8)
读事件SelectionKey.OP_READ(1)
写事件SelectionKey.OP_WRITE(4)
Java NIO 核心部分:
Channels
Buffers
Selectors
所有的 IO 在NIO 中都从一个Channel通道开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer
写到Channel中。

既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
通道可以异步地读写。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:



JAVA NIO中的一些主要Channel的实现:SocketChannel ,ServerSocketChannel。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

Java NIO里关键的Buffer实现:ByteBuffer
Java NIO 还有个 MappedByteBuffer,用于表示内存映射文件

使用Buffer读写数据一般遵循以下四个步骤:
写入数据到Buffer
调用
flip()
方法
从Buffer中读取数据
调用
clear()
方法或者
compact()
方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

Selector允许单线程处理多个 Channel,

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

这是在一个单线程中使用一个Selector处理3个Channel的图示:




Selector的创建

通过调用Selector.open()方法创建一个Selector,如下:
Selector selector = Selector.open();


向Selector注册通道

为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现,如下:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,
Selectionkey.OP_READ);


与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
Connect
Accept
Read
Write

通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。

这四种事件用SelectionKey的四个常量来表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:
打开一个SocketChannel并连接到互联网上的某台服务器。
一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。


打开 SocketChannel

下面是SocketChannel的打开方式:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));


关闭 SocketChannel

当用完SocketChannel之后调用SocketChannel.close()关闭SocketChannel:
socketChannel.close();


从 SocketChannel 读取数据

要从SocketChannel中读取数据,调用一个read()的方法之一。以下是例子:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);


首先,分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。

然后,调用SocketChannel.read()。该方法将数据从SocketChannel 读到Buffer中。read()方法返回的int值表示读了多少字节进Buffer里。如果返回的是-1,表示已经读到了流的末尾(连接关闭了)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: