Java NIO的总结 有更新!

 
评论 • 148 浏览

五一外面人山人海的,就在家研究了一下nio,以及reactor模型。记录一下,这篇是写nio的文章,还会有一篇是通过nio去实现reactor理念的文章。

java nio

在jdk1.4中引入NIO类,引入了一种基于新的I/O方式。
基于通道和缓冲区。性能优雅。
提供了堆外内存,选择器多路复用,非阻塞的IO方式。
大大加快了java的资源读写,网络通信的速度。
ps:后面打算再深入研究下netty。netty实现了自己的Bytebuf,并且堆外内存池化管理,实现了自己的FastThreadLocal,重写了channel等等。性能卓越,等我看完了会再写篇记录下。

阻塞与同步概念区别

关于NIO和BIO区别,这里不再赘述,请自行查阅相关文档。
这里有必要说明下,阻塞/非阻塞,同步/异步的概念区别。

阻塞、非阻塞是获取资源的方式。

  • 阻塞(blocking):资源不可用时,IO请求一直阻塞,直到返回结果(有数据或者超时)。
  • 非阻塞(non-blocking):资源不可用时,IO请求离开返回,返回数据标识不可用。(不管咋样直接返回)

同步、异步是程序设计的方式。

  • 同步(synchronous):是指应用在阻塞状态,一直等待直到数据成功传输或者返回失败。
  • 异步(asynchronous):应用发送或者接受数据后立即返回,实际处理是异步的。(比如,我们可以单开一个线程处理IO相关操作)

nio核心概念

nio有三个核心组件:

  • Buffer缓冲区
  • Channel通道
  • Selector选择器

Buffer缓冲区

缓冲区本质上是一个可以写入数据的内存块,然后可以读取。nio的channel必须要通过Buffer来进行交互。
在nio中,buffer本质上是一个数组,增加了相关api,使得操作数组更加简洁。

看下Buffer的类组成:
bufferpng

需要关注的是三个很重要的属性:

  • position:写入时代表写数据的位置,读取时代表读数据的位置。
  • limit:写入模式,limit等于capacity。读取模式下,limit代表已有的数据量。
  • capacity:作为内存(数组),Buffer有一个固定的大小。

bufferpng

在nio中,所有缓冲区类型都继承于抽象类buffer。类关系如下。
Bufferpng

buffer常见方法:

  • allocate()方法,申请一块内存。
  • allocateDirect()方法,申请一块堆外内存,返回的是DirectByteBuffer对象。
  • flip()读模式需要先调用
  • mark()标记一个位置。
  • reset()重置到mark的位置。
  • compact()清空已读取的数据。
堆外内存

nio中提供了堆内内存(HeapByteBuffer)和堆外内存(DirectByteBuffer)。
还有一种MappedByteBuffer,他是用来做内存映射的,本质上也是堆外内存,一般用来操作机器内存放不下的超大数据,这篇文章暂时不写,有兴趣自行查阅。

堆外内存的优点:

1:不需要拷贝进jvm堆,少了一次拷贝,提高了性能。也是很多0拷贝机制的重要基石。
2:GC范围之外,降低GC压力。手动的话需要调用cleaner的clean方法。GC的时候也会触发这个方法。

但是使用不当会造成内存泄漏,并且无法找到原因(因为不在堆内,所以很难监控)。

建议
1:在文件读写,网络传输的时候用堆外内存,提高效率。
2:通过虚拟机参数MaxDirectMemorySize限制堆外内存。

channel通道

channel是一个对象,通过channel可以与网络或者系统之间进行交互。channel需要和缓存区配合使用。

channelpng

nio中的channel:

  • ServerSocketChannel
  • SocketChannel
  • FileChannel
  • DatagramChannel
ServerSocketChannel

ServerSocketChannel是用来监听新建立的TCP连接。
一般可以这么使用:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
serverSocketChannel.configureBlocking(false);  
serverSocketChannel.socket().bind(new InetSocketAddress(port)); // 绑定端口

while (true) {  
     SocketChannel socketChannel = serverSocketChannel.accept();  
     if (socketChannel != null) {
     }
 }
SocketChannel

socketChannel用来建立TCP网络连接,来进行数据交互。
有两种方式获取:

  • 客户端主动发起
  • 服务端获取新的连接

客户端主动发起的方式:

SocketChannel socketChannel = SocketChannel.open();  
//设置为非阻塞  
socketChannel.configureBlocking(false);  
//重要,这是链接  
socketChannel.connect(new InetSocketAddress(ip, port));
...
socketChannel.close();

服务端获取新的连接:

SocketChannel socketChannel = serverSocketChannel.accept();

Selector选择器

selector选择器是NIO中一个核心。
selector选择器可以检查一个或者多个channel,并且确定哪些通道已经准备好进行读写。

selector做到了一个线程处理所有的IO事件。
selector实现的原理是,每一个channel都会通过SelectionKey注册到selector上,并且绑定每一个channel关注的事件。然后selector线程会去轮询这些SelectionKey,发现有channel准备就绪了就会返回该SelectionKey进行相关的读写操作。
因此实现了事件驱动机制,不需要同步的监听所有的事件。

Selector可以监听如下的事件:

  • SelectionKey.OP_CONNECT 连接
  • SelectionKey.OP_ACCEPT 准备就绪,accept事件
  • SelectionKey.OP_READ 读就绪
  • SelectionKey.OP_WRITE 写就绪

代码示例:

//构建一个selector

selector=Selector.open();  
//将socketChannel的accept事件注册到selector上
socketChannel.register(selector,SelectionKey.OP_ACCEPT,socketChannel);
...
while (true){  
    //selectNow()非阻塞,select(timeout)和select()阻塞  
    //轮询selector,看看有没有通道就绪
     int readyChannels = selector.selectNow();  
     if(readyChannels == 0){  
        continue;  
      }  
      // 获取事件  
      Set<SelectionKey> selectionKeys= selector.selectedKeys();  
  }
...
//通过key获取到绑定的channel
ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.attachment(); 

底层应该用到了操作系统的多路复用,epoll模型。
但是在系统高并发的情况下,单个selector也会成为性能瓶颈,这个时候有一种非常优秀的线程IO线程模型叫reactor模型可以大大提高性能。

评论
validate