JAVA NIO系列之NIO浅析

一、前言

java是一门跨平台的语言,因此能够一次编译到处运行。为了使java字节码能够在不同操作系统上运行,java设计必须做出妥协,必须选择各种平台都能接受的处理方案。因此妥协直接带来了java的性能问题,最直接受影响的就是IO领域。为了适应不同的操作系统,java抽象出一套IO类,这些类主要面向字节流数据。但为了执行这些字节流数据,就要执行好几层的方法调用。

这种面向对象的处理方法,将不同的I/O对象组合到一起,供了高度的灵活性,但需要处理大量数据时,却可能对执行效率造成致命伤害。当移动大量数据时,这些I/O类可伸缩性不强,也没有提供当今大多数操作系统普遍具备的常用I/O功能,如文件锁定、非块 I/O、就绪性选择和内存映射。这些特性对实现可伸缩性是至关重要的,对保持与非Java应用程序的正常交互也可以说是必不可少的,尤其是在企业应用层面,而传统的Java I/O机制却没有模拟这些通 用 I/O 服务。

为了克服这些问题java在JDK1.4中引入了NIO(New IO),使得操作系统强大的 I/O 特性终于可以借助Java供的工具得到充分发挥。 论及 I/O 性能,Java 再也不逊于任何一款编程语言。

二、IO

IO是什么?IO(I/O 输入和/输出)是在主存和外部设备(如磁盘驱动器、终端和网络)之间拷贝数据的过程。输入是从I/O设备拷贝数据到主存,而输出操作是从主存拷贝数据到I/O设备[来自《深入理解计算系统》的定义]。
所有的系统I/O都分为两个阶段:等待就绪和操作。在进行读写的时候需要等待IO设备读写就绪,只有准备就绪了才能进行读写操作。在系统等待就绪过程是最耗时间的,而操作过程是很快的,比如在单机上操作过程就是一个内存拷贝过程,在网络中就是一个数据传输过程。

所有的语言都提供了IO工具,在传统javaIO(JDK1.4以前版本)对于数据的输入/输出操作都是以“流”的方式进行。所谓的流是输入输出设备的抽象,如网络(网络流)、文件(文件流)。(更多参考:《深入理解 Java中的 流》)

传统JAVA IO有叫做BIO(Blocking I/O, 即同步阻塞IO)。这种方式基于字节流,在执行read()和write时读写条件不满足时会阻塞等待读写就绪。

而NIO(Non-blocking I/O,在Java领域,也称为New I/O,),是一种同步非阻塞的I/O模型,是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 这种方式在执行write()和read()的时,如果条件不满足(如没有数据时)会直接返回,而不会阻塞在那边。
另外还有一种IO是异步非阻塞IO,即AIO(Asynchronous I/O)。这种IO方式不会阻塞,同时也不需要采用监听方式一直去查询是否读写准备就绪。AIO一旦读写就绪后就会发送一个消息通知,免去等待和轮询。

三、传统BIO模型分析

操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。而 JVM 的 I/O 类(BIO)喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io 的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io 类则喜欢一铲子一铲子 地加工数据。因此导致数据处理效率低下,而java性能大部分也是受IO性能影响,CPU性能可以通过优化java代码提高运行性能。

另外BIO是一种阻塞IO,在读写的时候需要等待读写就绪,这个过程线程不能去执行其他事情,只能蛮目等待。在传统的网络编程中(基于),服务端需要为每个socket分配一个线程去监听和等待该socket的执行过程。如代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void runServer(){
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()) {//主线程死循环等待新连接到来
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
}

class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
String someThing = socket.read()....//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}

}
}
}

socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,如果是单线程则导致线程阻塞卡死,使得服务器处理效率大幅度降低,后面来的socket请求必须等待前面的socket处理完成。因此可以利用CPU多核的特性,使用多线程来完成。使得某个socket卡住时,其他socket都能够正常工作。但是多线程的引入也带来一些问题:

  1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
  2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
  3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
  4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。
引用(【美团技术博客】Java NIO浅析)

四、NIO

Java 程序员如何能够既利用操作系统的强大功能,又保持平台独立性?因为跨平台隐藏操作系 统的技术细节也意味着某些个性鲜明、功能强大的特性被挡在了门外。如果JDK提供API,直接调用Java 本地接口(JNI)编写本地代码,直接使用操作系 统特性,这种行为会破坏sun API的一致性规范。
为了解决这一问题,java.nio 软件包 供了新的抽象。具体地说,就是 Channel 和 Selector 类。它们 供了使用 I/O 服务的通用 API,JDK 1.4 以前的版本是无法使用这些服务的。但天下还是没有免费的午餐:您无法使用每一种操作系统的每一种特性,但是这些新类还是 供了强大的新框 架,涵盖了当今商业操作系统普遍 供的高效 I/O 特性。不仅如此,java.nio.channels.spi还供了新的服务供接口(SPI),允许接入新型通道和选择器,同时又不违反规范的一致性。

NIO的核心组件是通道(Channel)、缓冲区(Buffer)和选择器(Selector),通过这三个组件使得NIO具备高效的IO能力,并能成为解决高并发与大量连接、I/O处理问题的有效方式。

  • Buffer:缓冲区,一个Buffer对象是固定数量的数据的容器。原始数据元素组成的固定长度数组,封装在包含状态信息的对象中,存入缓冲区。缓冲区提供了一个会合点:通道既可提取放在缓冲区中的数据(写),也可向缓冲区存入数据供读取(读)。此外,还有一种特殊类型的缓冲区,用于内存映射文件。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程

  • Channel: 通道。可以把通道想象成连接缓冲区和 I/O 服务的捷径,是I/O传输发生时通过的入口。流是单向流动的(一个流必须是 InputStream 或者 OutputStream 的子类),通道是双向的,即可以读、写或同时进行读写操作。针对不同的IO设备,Chennel包含了FileChannel(文件),DatagramChannel(UDP)、SocketChannel(TCP)、ServerSocketChannel(网络IO)。
    FileChannel是阻塞channel,是唯一一个不支持非阻塞的通道。

  • Selector:选择器可实现就绪性选择。Selector类提供了确定一或多个通道当前状态的机制。使用选择器,借助单一线程,就可对数量庞大的活动 I/O 通道实施监控和维护。

举例:
如果把一个装满油并固定地上的油桶称作IO设备的话,那在同底下打个小小的洞口就是输出流(流只能往一个方向移动),而在桶的顶端打个小小的口用于添油,这个入口就是输入流。流都是以字节的方式进行,而且都是单向的,所以对应都是小口。而通道就是直接将桶盖拿去,用一个小桶去取油和放油。这个油桶的口就是通道,而小桶就是缓存buffer。因此小桶(buffer)可以通过油桶口(channel)取油或倒油。

三、NIO vs BIO

BIO和NIO的区别:

  1. BIO是基于流而NIO是基于块。
    BIO处理数据是每次都是从流中获取一个或多个字节,没有存放字节流的缓存,因此不能够前后移动数据。而NIO是将数据存放到一个缓冲区,并且可以对缓冲区做数据的移动、添加等操作,处理更加灵活。
  2. BIO是同步阻塞而NIO是同步非阻塞的
    BIO处理数据时,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。这种方式可以提高系统响应速度,降低内存和CPU资源的浪费和损耗。
  3. NIO具有选择器selector,而BIO则没有。
    NIO通过selecor,使得一个线程能够监听多个channel,这样可以节省线程的开支。也可以提高系统的响应速度。

参考

《java NIO》
【IBM技术博客】NIO 入门
【并发编程网站】Java NIO 系列教程
攻破JAVA NIO技术壁垒

Java IO流学习总结
深入理解 Java中的 流 (Stream)
【美团技术博客】Java NIO浅析
Java进阶(五)Java I/O模型从BIO到NIO和Reactor模式
【InfoQ】Java NIO通信框架在电信领域的实践

0%