目录
I/O概述
字符流
字节流
理解I/O超类结构
FileInputStream详解
FileOutputStream详解
File类
新I/O
Java NIO: Channels and Buffers
Channels
NIO FileChannel
SocketChannel
ServerSocketChannel
Buffers
Java NIO: Selectors
使用Selector的好处是什么?
Java NIO: Non-blocking IO
Java NIO vs. IO
Stream Oriented vs. Buffer Oriented
Blocking vs. Non-blocking IO
Selectors
NIO和IO总结
BIO,NIO和AIO
在jdk源码中,I/O是一个单独的包。I代表的是input输入,O代表的是output输出。java I/O就是指的java在输入输出上的技术。
一个程序针对一个信息源(如一个文件file,内存memory,一个端口socket)打开一个流,并且按照顺序的读取信息.如下图所示:
与读相似,一个程序也可以通过打开一个流并且按照顺序的写信息,来发送信息到一个外部的目的地去,如下所示:
一个流被定义为一个数据序列。输入流用于从源读取数据,输出流用于向目标写数据。无论数据来自何处或去往何处,也不论其类型如何,用于顺序读取和写入数据的算法基本相同:
Reading | Writing |
|
|
java.io包包含一组流类,这些流类支持这些用于读取和写入的算法。要使用这些类,程序需要导入Java.io。流类根据其操作的数据类型(字符或字节)分为两个类层次结构:Character Streams字符流,Byte Streams字节流。
Reader和Writer是java.io中字符流的抽象超类。Reader为读取读取16位字符的流提供了API和部分实现,而Writer为写入器(写入16位字符的流)提供API和部分实现。Reader和Writer的子类实现专用流,并被分成两类:从数据接收器读取或写入数据接收器(在下面的图中以灰色显示)和执行某种处理(以白色显示)的那些类:
大多数程序应该使用readers和writers来读和写文本信息。原因是它们可以处理Unicode字符集中的任何字符,而字节流仅限于ISO-Latin-1 8位字节。
为了读写8位字节,程序应该使用字节流、InputStream和OutputStream的后代。InputStream和OutputStream为输入流(读取8位字节的流)和输出流(写入8位字节的流)提供API和部分实现。这些流通常用于读取和写入二进制数据,例如图像和声音。对象流序列化中使用了两个字节流类ObjectInputStream
和ObjectOutputStream流。这些类在对象序列化中被覆盖。
与Reader和Writer一样,InputStream和OutputStream的子类提供了专门的I/O功能,可以分为两类,如下面的类层次图所示:数据汇流(灰色)和数据处理流(白色):
下图是一个描述输入流和输出流的类层次图:
Reader和
InputStream
定义了类似的API,但对于不同的数据类型。例如,Reader包含读取字符和字符数组的方法:
int read()
int read(char cbuf[])
int read(char cbuf[], int offset, int length)
InputStream
定义了相同的方法,但用于读取字节和字节数组:
int read()
int read(byte cbuf[])
int read(byte cbuf[], int offset, int length)
此外,Reader和InputStream都提供了用于标记流中的位置、跳过输入和重置当前位置的方法。
Writer和OutputStream
同样是类似的的。Writer
定义了用于书写字符和字符数组的方法
int write(int c)
int write(char cbuf[])
int write(char cbuf[], int offset, int length)
OutputStream
流定义了相同的方法,但对于字节:
int write(int c)
int write(byte cbuf[])
int write(byte cbuf[], int offset, int length)
所有流,读取器readers、写入器writers、输入流input streams和输出流output streams——都是在创建时自动打开的。可以通过调用它的关闭方法显式关闭任何流。或者垃圾收集器可以隐式关闭它,这是在不再引用对象时发生的(由垃圾收集器调用处理)。
该流用于从文件读取数据,它的对象可以用关键字 new 来创建。
有多种构造方法可用来创建对象
可以使用字符串类型的文件名来创建一个输入流对象来读取文件
InputStream f = new FileInputStream("C:/java/hello");
也可以使用一个文件对象来创建一个输入流对象来读取文件。我们首先得使用 File() 方法来创建一个文件对象:
File f = new File("C:/java/hello");
InputStream out = new FileInputStream(f);
创建了InputStream对象,就可以使用下面的方法来读取流或者进行其他的流操作:
public void close() throws IOException{} | 关闭此文件输入流并释放与此流有关的所有系统资源。抛出IOException异常。 |
protected void finalize()throws IOException {} | 这个方法清除与该文件的连接。确保在不再引用文件输入流时调用其 close 方法。抛出IOException异常。 |
public int read(int r)throws IOException{} | 这个方法从 InputStream 对象读取指定字节的数据。返回为整数值。返回下一字节数据,如果已经到结尾则返回-1。 |
public int read(byte[] r) throws IOException{} | 这个方法从输入流读取r.length长度的字节。返回读取的字节数。如果是文件结尾则返回-1。 |
public int available() throws IOException{} | 返回下一次对此输入流调用的方法可以不受阻塞地从此输入流读取的字节数。返回一个整数值。 |
该类用来创建一个文件并向文件中写数据。
如果该流在打开文件进行输出前,目标文件不存在,那么该流会创建该文件。
有两个构造方法可以用来创建 FileOutputStream 对象。
使用字符串类型的文件名来创建一个输出流对象:
OutputStream f = new FileOutputStream("C:/java/hello")
也可以使用一个文件对象来创建一个输出流来写文件。我们首先得使用File()方法来创建一个文件对象:
File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f);
创建OutputStream 对象完成后,就可以使用下面的方法来写入流或者进行其他的流操作。
public void close() throws IOException{} | 关闭此文件输入流并释放与此流有关的所有系统资源。抛出IOException异常。 |
protected void finalize()throws IOException {} | 这个方法清除与该文件的连接。确保在不再引用文件输入流时调用其 close 方法。抛出IOException异常。 |
public void write(int w)throws IOException{} | 这个方法把指定的字节写到输出流中。 |
public void write(byte[] w) | 把指定数组中w.length长度的字节写到OutputStream中。 |
创建目录
File类中有两个方法可以用来创建文件夹:
实现逻辑分析, mkdir方法的实现是通过native方法实现的,而mkdirs方法的实现逻辑则是借助于mkdir方法实现。
读取目录
一个目录其实就是一个 File 对象,它包含其他文件和文件夹。
如果创建一个 File 对象并且它是一个目录,那么调用 isDirectory() 方法会返回 true。
可以通过调用该对象上的 list() 方法,来提取它包含的文件和文件夹的列表。
下面展示的例子说明如何使用 list() 方法来检查一个文件夹中包含的内容:
删除目录或文件
删除文件可以使用 java.io.File.delete()方法。
在java1.4当中引入了新的java I/O类库,java.nio,java.nio, 全称java non-blocking IO,其目的在于提高速度, 新的io为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。实际上,旧的I/O包已经使用nio重新实现过了,以便充分利用这种速度提高。因此我们即便不显式的使用nio编写代码,也能从中受益。NI/O是用来替代标准IO的一个方案。
Java NIO提供了一种和标准IO API不同的方式来处理IO. JAVA NIO由以下核心组件组成:
在标准的IO API当中我们处理的是字节流和字符流,在NIO当中我们处理的是channels和buffers. channel和流有一点相似,数据总是从一个channel读到buffer里面,或者从一个buffer写到某个channel里面,如下图所示
JAVA NIO的channel和流类似但是有以下几个差异:
在java的nio包里面,主要的channel实现类如下:
我们可以分辨出来,这些channels的实现包含了UDP,TCP网络IO和 file IO。
FileChannel从文件files可以读写数据
DatagramChannel可以通过UDP协议从网络读写数据
DatagramChannel可以通过TCP协议从网络读写数据
ServerSocketChannel可以让你像web server一样监听发来的TCP连接。对于每一个发来的连接都会有一个SocketChannel被创建。
Java NIO的FileChannel是连接到文件的一个channel。使用FileChannel,可以读取文件中的数据,并将数据写入文件。JavaNIO文件通道类FileChannel是标准Java IO API读取文件功能的替代方案。
打开一个FileChannel
在使用FileChannel之前,必须打开它。不能直接打开FileChannel, 我们需要通过InputStream、OutputStream或RandomAccessFile获取FileChannel。下面是如何通过RandomAccessFile打开FileChannel的例子代码:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
从FileChannel读取数据
若要从FileChannel读取数据,需要调用其中一个read()方法。下面是一个例子:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
首先我们分配了一个Buffer,然后从FileC含泥量读到的数据会读入到Buffer里面。
其次需要调用FileChannel的read方法,这个方法会把FileChannel中的data读取到Buffer当中去。返回的int类型的值表明了有多少bytes的数据被写入了Buffer.如果返回了-1,那么说明读到了文件结束的地方(?)
把data写入FileChannel
写入数据到FileChannel是通过调用FileChannel的write方法完成的,这个方法会接收Buffer作为它的参数.下面是一个例子:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
注意,FileChannel的write方法是在一个while循环内部被调用的。没有一个保证有多少bytes的数据通过write方法被写入到了FileChannel当中。因此我们不停的反复调用write方法直到没有更多的数据需要写入。
关闭一个FileChannel
在使用完FileChannel之后我们必须关闭它,关闭的代码如下:
channel.close();
FileChannel的大小
FileChannel的size方法返回这个FileChannel连接到的file文件的大小:
long fileSize = channel.size();
文件截断
您可以通过调用 FileChannel.truncate()
方法截断文件。截断文件时,按给定长度切断文件。
channel.truncate(1024);
这个示例以长度为1024个字节截断文件。
FileChannel Force
FileChannel的force方法将所有未写入数据从通道刷新到磁盘。由于性能原因,操作系统可能会在内存中缓存数据,所以在调用.force()方法之前,不能保证写入通道的数据实际上是写入磁盘的。
channel.force(true);
下面是一个使用FileChannel把数据读到Buffer的例子:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Java NIO SocketChannel 是连接到TCP网络套接字的channel实现。它是和Java网络的套接字一样的Java NIO。创建SocketChannel 有两种方法:
SocketChanne的一些基本用法:
//打开一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
socketChannel.close(); //关闭一个SocketChannel
//从一个SocketChannel读数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
写数据到一个SocketChannel
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
注意,SocketChannel的write方法是在一个while循环内部被调用的。没有一个保证有多少bytes的数据通过write方法被写入到了SocketChannel当中。因此我们不停的反复调用write方法直到没有更多的数据需要写入。这一点与FileChannel是一样的。
Non-blocking Mode非阻塞模式
可以将SocketChannel设置为非阻塞模式。这样设置之后,可以在异步模式下调用connect()、Read()和write()方法。
Non-blocking Mode 和Selectors
SocketChannel的非阻塞模式和Selector一起工作得更好。通过向Selector注册一个或多个SocketChannel,您可以向Selector请求已经准备好 reading, writing的通道。
Java NIO ServerSocketChannel是一个可以侦听传入的TCP连接的通道,就像标准Java网络中的ServerSocket 一样。ServerSocketChannel 类位于JavaNio.channels 包中。下面是一个例子:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
一些基本用法
//打开一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//关闭一个ServerSocketChannel
serverSocketChannel.close();
//监听连接
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
Non-blocking Mode非阻塞模式
可以将ServerSocketChannel设置成非阻塞模式。在非阻塞模式下,accept()方法立即返回,如果没有传入连接则返回空值。因此,您必须检查返回的SOCKET通道是否为NULL。下面是一个例子:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}
下面是Java NIO中核心的Buffer实现的列表:
这些Buffer的实现类覆盖了可以通过IO发送的基本数据类型,包括:byte, short, int, long, float, double 和 characters。
当我们和NIO的channel交互式需要用到Buffers。数据是从channel读入缓冲区buffer,并从缓冲区buffer写入通道。
Buffer基本用法
使用缓冲区Buffer读取和写入数据通常遵循这4步:
调用buffer.flip()方法
buffer.clear()
或者 buffer.compact()方法
当你将数据写入缓冲区时,缓冲区会记录你已经写入了多少数据。一旦需要读取数据的时候,就需要调用flip()方法将缓冲区从写入模式(writing model)切换到读取模式(reading model)。在读取模式下,缓冲区允许你读取已经写入缓冲区的所有数据。
一旦读取了所有数据,就需要清除缓冲区,以便再次准备写入。您可以通过两种方式来执行这一操作:调用clear()方法或调用compact()方法。clear方法将会清除整个缓冲区。compact方法只清除您已经读取的数据。所有的未读数据都会被移动到缓冲器的开头,并且新写入的数据将被写入在缓冲区的未读数据之后。
下面是一个简单的Buffer使用示例,包含了write, flip, read and clear 方法:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
Buffer 容量、位置和极限
缓冲区本质上是一个内存块,您可以在其中写入数据,然后再读取。这个内存块包装在一个NIO缓冲对象中(NIO Buffer object),该对象提供了一系列的方法来让我们更加轻易的使用内存块。
缓冲区有三个需要熟悉的属性,可以帮助我们理解缓冲区是如何工作的:
position和limit的含义取决于Buffer是处于读还是写模式。不管buffer模式如何,capacity容量总是相同的。
下面是写和读模式下的容量、位置和限制的说明:
Capacity
作为一个内存块,缓冲器具有一定的固定大小,也称其“容量”。只能将容量字节、长度、字符等写入缓冲区。一旦缓冲区满了,您就需要清空它(读取数据,或清除数据),然后再写入更多的数据。
position
当您将数据写入缓冲区时,您在某个位置上这样做。最初的位置是0。当字节、长等被写入缓冲区时,位置被提前指向缓冲区中的下一个单元格,以便将数据插入其中。位置可以最大限度地变为容量1。
当你从缓冲区读取数据时,你也可以从给定的位置读取数据。当将缓冲区从写入模式翻转到读取模式时,该位置被重置为0。当您从缓冲区读取数据时,您从位置读取数据,然后将位置提前到下一个位置进行读取。
limit
在写入模式中,缓冲区的限制是可以写入缓冲区的数据量的限制。在写入模式中,限制等于缓冲器的容量。
当将缓冲区翻转为读取模式时,限制意味着可以从数据中读取多少数据的限制。因此,当将缓冲器翻转为读取模式时,限制设置为写入模式的写入位置。换句话说,您可以读取与写入的字节数一样多的字节(限制设置为写入的字节数,以位置标记)。
分配一个Buffer
下面的代码是一个分配一个固定容量的buffer的例子
//Here is an example showing the allocation of a ByteBuffer, with a capacity of 48 bytes:
ByteBuffer buf = ByteBuffer.allocate(48);
//Here is an example allocating a CharBuffer with space for 1024 characters:
CharBuffer buf = CharBuffer.allocate(1024);
Java NIO包含“selectors”的概念。一个selector是一个可以监视多个通道channels的事件(例如:连接打开、数据到达等)的对象。因此,单个线程可以监视多个通道channels以获取数据,例如,在聊天软件的服务器中。
使用单个线程来处理多个通道channels的优点是只需要更少的线程来处理通道。实际上,我们只能使用一个线程来处理所有的通道。线程之间的切换对于操作系统来说是昂贵的,并且每个线程也占用了操作系统中的一些资源(内存)。因此,使用的线程越少越好。
我们需要记住的是,现代操作系统和CPU在多任务中变得越来越好,因此多线程的开销随着时间的推移变得越来越小。事实上,如果CPU有多个内核,你不使用多线程处理的话可能会浪费CPU功率。不管怎样,设计讨论属于不同的文本。这里我们只考虑使用单个线程通过一个Selector来处理多个channels,。
下面是一个线程通过使用一个Selector来处理3个Channel
的说明:
要使用Selector就要用它去注册一个Channel。然后你可以call这个Selector的select()方法。此方法将阻塞,直到接收到一个注册的channel准备好的event。一旦方法返回,线程就可以处理这些事件。Event的例子有传入连接、数据接收等。
创建一个Selector
我们可以通过调用 Selector.open()
方法来创建一个Selector的实例
Selector selector = Selector.open();
注册channel到一个Selector
为了使用Selector和Channel,必须用Selector注册Channel。这是使用SelectableChannel.register()方法完成的,如下所示:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
通道必须处于非阻塞模式non-blocking mode,以便与Selector
一起使用。这意味着不能使用Selector
和FileChannel
,因为文件FileChannel
不能切换到非阻塞模式。Socket channels比较适用于这种场景。
注意register()
方法的第二个参数(即上面的SelectionKey.OP_READ)。这是一个interest set,意思是通过Selector针对一个channel你想监听的events事件是什么。有四个不同类型的事件可以监听
一个成功连接上另一个server的channel的状态就是"connect" ready, 一个server的socket channel接收了一个连接之后的状态就是'Accept' ready状态. 一个channel如果有准备好被读取的data那么它的状态就是'read' ready,一个channel的状态是准备好可以写入data,那么它的状态就是‘write’ ready.
这四个事件有四个对应的SelectionKey
常数表示,分别是:
如果你想监听不止一个事件,则可以按照下面这种方式来定义
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
下面的代码是一个完整的Selector的用法的例子:
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}
}
Java NIO使你能够使用非阻塞IO。例如,线程可以让一个channel将数据读入某个缓冲区buffer当中。当通道channel将数据读入缓冲区时,线程可以做其他事情。一旦数据读入缓冲区buffer,线程就可以继续处理它。将数据写入信道也是一样的。
下表总结了Java NIO和IO之间的主要区别
IO | NIO |
Stream oriented 面向流 | Buffer oriented面向缓冲 |
Blocking IO阻塞式IO | Non blocking IO非阻塞式IO |
Selectors |
Java NIO和IO之间的第一个最大区别是IO是面向流的,其中NIO是面向缓冲区的。那么,这意味着什么呢?
以流为导向的Java IO意味着每次从一个流读取一个或多个字节。你用读字节做什么取决于你。它们不在任何地方缓存。此外,不能在流中的数据中来回移动。如果需要在数据流中来回移动,则需要先在缓冲区中缓存数据。
Java NIO的面向缓冲的方法略有不同。将数据读入缓冲区,从中缓冲区稍后进行处理。您可以在缓冲区中来回移动,如您需要的那样。这使您在处理过程中有更多的灵活性。但是,您还需要检查缓冲区是否包含您需要的所有数据以便完全处理它。而且,您需要确保在向缓冲区读取更多数据时,不会覆盖尚未处理的缓冲区中的数据。
Java IO的各种流正在阻塞。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有要读取的数据或数据被完全写入为止。同时线程也不能做任何其他事情。
JAVA NIO的非阻塞模式允许线程请求从通道读取数据,并且只获取当前可用的内容,或者根本没有任何数据,如果当前没有可用的数据。线程可以继续进行其他操作,而不是在数据变得可读取之前保持阻塞。
对于非阻塞写作也是如此。一个线程可以请求一些数据写入一个通道,而不是等待它被完全写入。然后线程可以继续运行,同时做一些其他的事情。
当线程在IO调用中未被阻塞时,它们的空闲时间通常在其他信道上执行IO。也就是说,单个线程现在可以管理多个输入和输出通道。
JAVA NIO的Selectors允许单个线程监视多个输入通道。您可以用Selectors注册多个通道,然后使用单个线程来“选择”具有可供处理的输入的通道,或者选择准备写入的通道。这个Selectors机制使单个线程易于管理多个通道。
NIO允许您仅使用一个(或几个)线程来管理多个通道(网络连接或文件),但代价是解析数据可能比从阻塞流读取数据要复杂一些。
如果需要同时管理数千个打开的连接(每个连接只发送少量数据,例如聊天服务器),那么在NIO中实现服务器可能是一个优势。类似地,如果您需要保持到其他计算机的大量开放连接,例如,在P2P网络中,使用单个线程来管理所有出站连接可能是一个优势。这一个线程,多个连接设计在这个图中说明:
如果具有较高带宽的连接较少,一次发送大量数据,那么经典的IO服务器实现可能是最合适的。这个图说明了一个经典的IO服务器设计:
BIO : Blocking IO即阻塞IO,即传统的java IO技术。
NIO:new IO。同步非阻塞IO。在jdk1.4之后引入。详细信息在上面已经介绍。
国内有很多技术博客将英文翻译成No-Blocking I/O,非阻塞I/O模型 ,当然这样就与BIO形成了鲜明的特性对比。NIO本身是基于事件驱动的思想来实现的,其目的就是解决BIO的大并发问题,在BIO模型中,如果需要并发处理多个I/O请求,那就需要多线程来支持,NIO使用了多路复用器机制,以socket使用来说,多路复用器通过不断轮询各个连接的状态,只有在socket有流可读或者可写时,应用程序才需要去处理它,在线程的使用上,就不需要一个连接就必须使用一个处理线程了,而是只是有效请求时(确实需要进行I/O处理时),才会使用一个线程去处理,这样就避免了BIO模型下大量线程处于阻塞等待状态的情景。
AIO:Asynchronous I/O,异步非阻塞I/O。在jdk1.7中引入。
Java AIO就是Java作为对异步IO提供支持的NIO.2 ,Java NIO2 (JSR 203)定义了更多的 New I/O APIs, 提案2003提出,直到2011年才发布, 最终在JDK 7中才实现
本质为实现两个进程之间的通信
参考: https://www.math.uni-hamburg.de/doc/java/tutorial/essential/io/overview.html
http://www.jfox.info/java-nio-jieshao.html