现在运用NIO的场景越来越多,许多网上的技术结构或多或少的运用NIO技术,比方Tomcat,Jetty。 学习和把握NIO技术现已不是一个JAVA攻城狮的加分技术,而是一个必备技术。再者,现在互联网的面试中上点level的都会触及一下NIO或许AIO的问题(AIO下次再叙述,本篇首要叙述NIO),把握好NIO也能协助你取得一份较好的offer。 唆使博主写这篇文章的要害是网上关于NIO的文章并不是许多,并且事例较少,针对这个特性,本文首要经过实践事例首要叙述NIO的用法,每个事例都经过实践查验。博主经过自己的了解以及一些事例期望能给各位在学习NIO之时多一份参阅。博主才能有限,文中有不足之处欢迎之处。

概述

NIO首要有三大中心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO根据字节省和字符流进行操作,而NIO根据Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或许从缓冲区写入到通道中。Selector(挑选区)用于监听多个通道的工作(比方:衔接翻开,数据抵达)。因而,单个线程能够监听多个数据通道。

NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取一切字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。假如需求前后移动从流中读取的数据,需求先将它缓存到一个缓冲区。NIO的缓冲导向办法略有不同。数据读取到一个它稍后处理的缓冲区,需求时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。可是,还需求查看是否该缓冲区中包括一切您需求处理的数据。并且,需保证当更多的数据读入缓冲区时,不要掩盖缓冲区里没有处理的数据。

IO的各种流是堵塞的。这意味着,当一个线程调用read() 或 write()时,该线程被堵塞,直到有一些数据被读取,或数据彻底写入。该线程在此期间不能再干任何工作了。 NIO的非堵塞形式,使一个线程从某通道发送恳求读取数据,可是它仅能得到现在可用的数据,假如现在没有数据可用时,就什么都不会获取。而不是坚持线程堵塞,所以直至数据变的能够读取之前,该线程能够持续做其他的工作。 非堵塞写也是如此。一个线程恳求写入一些数据到某通道,但不需求等候它彻底写入,这个线程一起能够去做其他工作。 线程通常将非堵塞IO的闲暇时刻用于在其它通道上履行IO操作,所以一个独自的线程现在能够办理多个输入和输出通道(channel)。

Channel

首要说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,比方:InputStream, OutputStream.而Channel是双向的,既能够用来进行读操作,又能够用来进行写操作。 NIO中的Channel的首要完成有:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel 这儿看姓名就能够猜出个所以然来:别离能够对应文件IO、UDP和TCP(Server和Client)。下面演示的事例根本上便是环绕这4个类型的Channel进行陈说的。

Buffer

NIO中的要害Buffer完成有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,别离对应根本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这儿先不进行陈说。

Selector

Selector运转单线程处理多个Channel,假如你的运用翻开了多个通道,但每个衔接的流量都很低,运用Selector就会很便利。例如在一个谈天服务器中。要运用Selector, 得向Selector注册Channel,然后调用它的select()办法。这个办法会一向堵塞到某个注册的通道有工作安排妥当。一旦这个办法回来,线程就能够处理这些工作,工作的比如有如新的衔接进来、数据接纳等。

FileChannel

看完上面的陈说,关于第一次触摸NIO的同学来说云里雾里,只说了一些概念,也没记住什么,更甭说怎样用了。这儿开端经过传统IO以及更改后的NIO来做比照,以更形象的杰出NIO的用法,进而使你对NIO有一点点的了解。

传统IO vs NIO

首要,事例1是选用FileInputStream读取文件内容的:

public static void method2(){

InputStream in = null;

try{

in = new BufferedInputStream(new FileInputStream("src/nomal_io.txt"));

byte [] buf = new byte[1024];

int bytesRead = in.read(buf);

while(bytesRead != -1)

{

for(int i=0;i

System.out.print((char)buf[i]);

bytesRead = in.read(buf);

}

}catch (IOException e)

{

e.printStackTrace();

}finally{

try{

if(in != null){

in.close();

}

}catch (IOException e){

e.printStackTrace();

}

}

输出成果:(略)

事例是对应的NIO(这儿经过RandomAccessFile进行操作,当然也能够经过FileInputStream.getChannel()进行操作):

 public static void method1(){
RandomAccessFile aFile = null;
try{
aFile = new RandomAccessFile("src/nio.txt","rw");
FileChannel fileChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buf);
System.out.println(bytesRead);
while(bytesRead != -1)
{
buf.flip();
while(buf.hasRemaining())
{
System.out.print((char)buf.get());
}
buf.compact();
bytesRead = fileChannel.read(buf);
}
}catch (IOException e){
e.printStackTrace();
}finally{
try{
if(aFile != null){
aFile.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}

输出成果:(略) 经过细心比照事例1和事例2,应该能看出个大约,最起码能发现NIO的完成办法比叫杂乱。有了一个大约的形象能够进入下一步了。

Buffer的运用

从事例2中能够总结出运用Buffer一般遵从下面几个过程:

  • 分配空间(ByteBuffer buf = ByteBuffer.allocate(1024); 还有一种allocateDirector后边再陈说)
  • 写入数据到Buffer(int bytesRead = fileChannel.read(buf);)
  • 调用filp()办法( buf.flip();)
  • 从Buffer中读取数据(System.out.print((char)buf.get());)
  • 调用clear()办法或许compact()办法 Buffer望文生义:缓冲区,实践上是一个容器,一个接连数组。Channel供给从文件、网络读取数据的途径,可是读写的数据都必须经过Buffer。如下图:

向Buffer中写数据:

从Channel写到Buffer (fileChannel.read(buf))

  • 经过Buffer的put()办法 (buf.put(…))
  • 从Buffer中读取数据:

从Buffer读取到Channel (channel.write(buf))

  • 运用get()办法从Buffer中读取数据 (buf.get())
  • 能够把Buffer简略地了解为一组根本数据类型的元素列表,它经过几个变量来保存这个数据的当时方位状况:capacity, position, limit, mark:

索引阐明capacity缓冲区数组的总长度position下一个要操作的数据元素的方位limit缓冲区数组中不行操作的下一个元素的方位:limit<=capacitymark用于记载当时position的前一个方位或许默许是-1

图无本相,举例:咱们经过ByteBuffer.allocate(11)办法创建了一个11个byte的数组的缓冲区,初始状况如上图,position的方位为0,capacity和limit默许都是数组长度。当咱们写入5个字节时,改变如下图:

这时咱们需求将缓冲区中的5个字节数据写入Channel的通讯信道,所以咱们调用ByteBuffer.flip()办法,改变如下图所示(position设回0,并将limit设成之前的position的值):

这时底层操作系统就能够从缓冲区中正确读取这个5个字节数据并发送出去了。鄙人一次写数据之前咱们再调用clear()办法,缓冲区的索引方位又回到了初始方位。

调用clear()办法:position将被设回0,limit设置成capacity,换句话说,Buffer被清空了,其实Buffer中的数据并未被清楚,仅仅这些符号通知咱们能够从哪里开端往Buffer里写数据。假如Buffer中有一些未读的数据,调用clear()办法,数据将“被忘记”,意味着不再有任何符号会通知你哪些数据被读过,哪些还没有。假如Buffer中仍有未读的数据,且后续还需求这些数据,可是此刻想要先先写些数据,那么运用compact()办法。compact()办法将一切未读的数据拷贝到Buffer开始处。然后将position设到最终一个未读元素正后边。limit特点依然像clear()办法相同,设置成capacity。现在Buffer预备好写数据了,可是不会掩盖未读的数据。

经过调用Buffer.mark()办法,能够符号Buffer中的一个特定的position,之后能够经过调用Buffer.reset()办法康复到这个position。Buffer.rewind()办法将position设回0,所以你能够重读Buffer中的一切数据。limit坚持不变,依然表明能从Buffer中读取多少个元素。

推荐阅读