什么是NIO
- NIO是 New I/O的简称,与旧式的基于流的I/O方法相对,也被称为非阻塞IO,它是一套新的I/O标准,在jdk1.4中被发布
NIO的特性
- 原始的I/O是基于的流的方式,而NIO是基于块(Block)的,它以块为单位来对数据进行处理,因为本身硬盘上的文件就是使用块进行存储的,NIO对此进行了对应,所以NIO的性能上比原始I/O要好的多。
- 为所有的原始类型提供(buffer)缓存支持
- 增加通道(Channel)对象,作为新的原始I/O抽象(可以说是代替以往的流)
- 支持锁和内存映射文件的文件访问接口
- 提供了基于selector的异步网络I/O
NIO的核心组成部分
Buffer
- Buffer的本质实际是一块内存区域,通过native函数库分配堆外内存,然后通过存储在对堆中的DirectByteBuffer对象作为该内存的引用对象,这一实现将该内存区域与java运行时数据区进行分离,并且又通过堆内对象进行调用。
- Buffer的子类实现:

- Buffer中的3个重要的参数:
- position(位置):
- 写模式:当前缓冲区的位置,将从position的下一个位置写数据。
- 读模式:当前缓冲区的读取的位置,将从此位置后读取数据
- capactiy(容量)
- limit(上限)
- 写模式:缓冲区的实际上限,它总是小于等于容量,通常情况下与容量是相等的
- 读模式:代表可读取的容量,和上次写入的数据量相等。
- Buffer中的重要方法:
- rewind():将position置为零,并清除标志位(mark),用于重新读取buffer中的数据。
- clear():将position置零,同时将limit的值设置与容量capactiy相等,并清除标志mark。
- flip():用于读写转化是使用,先将limit设置位position所在的位置,再将position置为零,并清除标记位mark
- mark():可以标记Buffer中的一个特定position。
- reset():跟mark()联合使用,调用该方法后可以恢复到mark标记的position位置上。
Channels
- Channels的主要实现:
- FileChannel:从文件中读取数据
- DatagramChannel:通过UDP协议读取网络中的数据
- SocketChannel:通过TCP协议读取网络中的数据
- ServerSocketChannel:可以监听新建来的TCP连接,像web服务器一样,对每一个新进来的连接创建一个SocketChannel
- Channels与流形式对比:
- Channels跟流还是比较类似的,但是流的读写一般时单向的,而 Channels则可以读也可以写。
- Channels支持异步的读取和写入,而流必须同步进行
- Channels中的数据读取或写入都需要buffer来进行周转。
- API解析应用:
- Channel的分散和聚集:
- scatter(分散):Channels允许在读取通道数据的时,将数据读取到多个Buffer中。
private static void scatterRead(File source)throws Exception{
RandomAccessFile raf=null;
try {
raf = new RandomAccessFile(source, "rw");
FileChannel channel = raf.getChannel();
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocate(8192);
ByteBuffer[] array = new ByteBuffer[]{buffer1, buffer2};
channel.read(array);
}finally { raf.close(); }
}
- Gather(聚集):Channels允许在往通道中写入数据时,写入多个Buffer中的数据。
private static void aggregateWrite(File source)throws Exception{
RandomAccessFile raf=null;
try {
raf = new RandomAccessFile(source, "rw");
FileChannel channel = raf.getChannel();
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocate(2048);
ByteBuffer[] array = new ByteBuffer[]{buffer1, buffer2};
channel.write(array);
}finally { raf.close(); }
}
- FileChannel之间的数据传输
- transferForm():
private static void dataConversion1(File source,File target)throws Exception{
RandomAccessFile from=null;
RandomAccessFile to=null;
try {
from = new RandomAccessFile(source, "rw");
to = new RandomAccessFile(target, "rw");
FileChannel fromChannel=from.getChannel();
FileChannel toChannel=to.getChannel();
long count=fromChannel.size();
toChannel.transferFrom(fromChannel,0,count);
}finally { from.close(); to.close(); }
}
- transferTo():
private static void dataConversion2(File source,File target)throws Exception{
RandomAccessFile from=null;
RandomAccessFile to=null;
try {
to = new RandomAccessFile(target, "rw");
from = new RandomAccessFile(source, "rw");
FileChannel toChannel=from.getChannel();
FileChannel fromChannel=from.getChannel();
long count=fromChannel.size();
fromChannel.transferTo(0,count,toChannel);
}finally { from.close(); to.close(); }
}
- Buffer+FileChannel 的简单应用
private static void copyFile(File source, File target){
FileInputStream in=null;
FileOutputStream out=null;
try {
in=new FileInputStream(source);
out=new FileOutputStream(target);
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
while (true){
try {
buffer.clear();
int read = inChannel.read(buffer);
if(read==-1) break;
buffer.flip();
outChannel.write(buffer);
} catch (Exception e) { e.printStackTrace(); }
}
} catch (FileNotFoundException e) { e.printStackTrace();
}finally {
try {
in.close();
out.close();
} catch (IOException e) { e.printStackTrace(); }
}
}
- DatagramChannel
- DatagramChannel使用的时UDP协议进行数据包传输,因为UDP是无连接的网络协议不需要像TCP一样进行连接,所以不同通过建立通道的方式进行数据传输,而是通过数据包的发送和接收进行传输。需要注意的是DatagramChannel接收数据时,如果buffer容不下收到的数据,多出的那部分数据就会被丢弃。
- 实例应用:
Selectors(选择器)
- 选择器允许单线程对多个通道进行管理,对于传统的IO模式而言,每一个IO都需要一个线程来进行处理,增大了服务器的负载,随着线程的增多线程之间的上下文切换开销也会变大,并且如果数据没有准备好线程还会进入阻塞状态,要等待数据准备好之后再对数据进行处理。而选择器的出现则解决了该问题,将通道注册到选择器中,一旦数据到达准备好之后再通知选择器对该数据进行处理,而当数据没有到达的时候线程不必等待可以去完成其他的任务。
- 传统IO处理方式

- NIO处理方式

- 运行原理:服务端将所有的Channel注册到Selector,Selector则负责监视这些channel的IO状态,任何一个或多个Channel准备好可用的IO操作后,Selector调用select()将返回有多少个Channel处于就绪状态,并通过selectedKeys()方法获取到这些Channel对应的SelectionKey集合,每一个SelectionKey都代表了一个Channel,再通过SelectionKey对某个channel进行IO操作。
- API解析
- 方法调用:
- select():阻塞到至少有一个Channel就绪后,返回就绪的Channel数量
- select(long timeout):阻塞立即返回,没有就绪Channel则返回0,每隔timeout毫秒后再次阻塞获取。
- selectNow():不会进行阻塞,不管是否有就绪的Channel;
- selectorKeys():返回就绪Channel的SelectionKey集合
- wakeUp():让Selector在调用select()后处于阻塞状态的线程立即返回不用等待Channel就绪后返回。
- 状态说明:上面提到Selector会判断channel处于就绪状态才会进行处理,在面介绍下Channel的状态。
- SelectionKey.OP_CONNECT:Channel成功连接服务器后进入连接就绪状态
- SelectionKey.OP_ACCEPT:服务器监听到了客户端的channel,服务器可以接收该连接
- SelectionKey.OP_READ:Channel中具有可读的数据后进入读就绪状态
- SelectionKey.OP_WRITE:Channel等待数据写入进入写就绪状态
- Selector+ServerSocketChannel+Buffer的简单应用:
拓展
RandomAccessFile与内存映射文件
RandomAccessFile
- RandomAccessFile是用来对文件内容进行随机访问的类,你可以通过设置你需要访问的位置对文件内容定位,当你在对文件进行读写的时候就是从你设置的位置开始读写。
- RandomAccessFile设定的四种对文件的访问模式
- r:以只读的方式打开文件,不可对文件进行写的操作
- rw:可以对文件进行读写操作
- rws:进行写操作时,同步刷新到磁盘,刷新内容和元数据(也就是该文件的注册信息之类)
- rwd:进行写操作时,同步刷新到磁盘,刷新内容
- RandomAccessFile运用了很多关于输入流输出流的方法,但是并非是FileInputStream和FileOutputStream,而是自己从零开始,不与输入输出流之间产生联系,因为其内部需要实现在文件内进行随机读写,在行为上与输入输出流有底层有所不同,所以RandomAccessFile是一个独立的类。
内存映射文件(memory-mapped files)
- 上面提到的RandomAccessFile可以对文件进行随机访问,这样就可以对文件进行修改增加操作;而在jdk1.4后javaNIO通过内存映射文件取带了RandomAccessFile的绝大部分功能
- 内存映射文件通过将文件映射到内存中,通过修改内存数据映射到文件的修改;内存文件映射要求必须指明文件映射开始位置,和映射数据范围,所以你映射的可以是大文件中的一个小片段。但是文件最大不得超过2GB。
- 在内存映射之前,都必须通过RandomAccessFile来对文件进行输出,可能是在内存映射文件时因为指定位置的关系,所以通过RandomAccessFile对随机访问提供支持吧。
- 内存映射文件应用:
private static void fileMappedMemory(File source){
RandomAccessFile raf=null;
try {
raf=new RandomAccessFile(source,"rw");
FileChannel fc=raf.getChannel();
MappedByteBuffer mbb=fc.map(FileChannel.MapMode.READ_WRITE,0,raf.length());
while (mbb.hasRemaining()){ System.out.println(mbb.get()); }
mbb.put(0,(byte)98);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Pipe(管道)
- 介绍:NIO的管道,用于两个线程之间的单向数据连接。pipe具有一个source通道和一个sink通道,数据会先写入到sink通道中,从source通道中进行读取。
- pipe原理图:

3.实例应用:public class PipeTest {
public static void main(String[] args) {
try {
Pipe pipe= Pipe.open();
Thread td1=new Thread(new WriteThread(pipe));
Thread td2=new Thread(new ReadThread(pipe));
td2.start();
td1.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class WriteThread implements Runnable{
private Pipe pipe;
public WriteThread(Pipe pipe) {
this.pipe = pipe;
}
@Override
public void run() {
Pipe.SinkChannel sinkChannel=null;
try {
sinkChannel=pipe.sink();
ByteBuffer buffer=ByteBuffer.allocate(1024);
Thread.sleep(10000);
buffer.put(Thread.currentThread().getName().concat("写入数据").getBytes());
buffer.flip();
sinkChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
sinkChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class ReadThread implements Runnable{
private Pipe pipe;
public ReadThread(Pipe pipe) {
this.pipe = pipe;
}
@Override
public void run() {
Pipe.SourceChannel sourceChannel=null;
try {
sourceChannel=pipe.source();
ByteBuffer buffer=ByteBuffer.allocate(1024);
sourceChannel.read(buffer);
System.out.println(new String(buffer.array()));
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
sourceChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}