NIO的介绍

众所周知为了提高Java I/O的速度和效率,从JDK1.4开始引入了java.nio.*包,即java new I/O(NIO),它的出现让许多原本还使用BIO的许多网站都立即修改了代码,那么BIO和NIO都有什么区别呢!

事实上,为了利用java nio的速度和效率优势,原来的java I/O包中相关的类已经使用java nio重新实现,因此在编程中即使没有显式地使用java nio的代码,使用传统java I/O还是利用了nio的速度和效率优势。Java nio的速度提高主要在:文件I/O和网络I/O两个方面。 摘自:JAVA编程思想NewI/O部分

通俗的说:就是BIO中每建立一个Socket连接时,同时创建一个新线程对该Socket进行单独通信(采用阻塞的方式通信)。这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况

NIO是服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发现某个Socket端口上有数据可读时(读就绪),则调用该socket连接的相应读操作;如果发现某个 Socket端口上有数据可写时(写就绪),则调用该socket连接的相应写操作;如果某个端口的Socket连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到了很大提高。使用了反应器模式 

传统的阻塞式IO,每个连接必须要开一个线程来处理,并且没处理完线程不能退出。

非阻塞式IO,由于基于反应器模式,用于事件多路分离和分派的体系结构模式,所以可以利用线程池来处理。事件来了就处理,处理完了就把线程归还。而传统阻塞方式不能使用线程池来处理,假设当前有10000个连接,非阻塞方式可能用1000个线程的线程池就搞定了,而传统阻塞方式就需要开10000个来处理。如果连接数较多将会出现资源不足的情况。非阻塞的核心优势就在这里。

为什么会这样,下面就对他们做进一步细致具体的分析:

首先,我们来分析传统阻塞式IO的瓶颈在哪里。在连接数不多的情况下,传统IO编写容易方便使用。但是随着连接数的增多,问题传统IO就不行了。因为前面说过,传统IO处理每个连接都要消耗 一个线程,而程序的效率当线程数不多时是随着线程数的增加而增加,但是到一定的数量之后,是随着线程数的增加而减少。这里我们得出结论,传统阻塞式IO的瓶颈在于不能处理过多的连接。

然后,非阻塞式IO的出现的目的就是为了解决这个瓶颈。而非阻塞式IO是怎么实现的呢?非阻塞IO处理连接的线程数和连接数没有联系,也就是说处理10000个连接非阻塞IO不需要10000个线程,你可以用1000个也可以用2000个线程来处理。因为非阻塞IO处理连接是异步的。当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求"事件",并把这个"事件"分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还。这样一个线程就可以异步的处理多个事件。而阻塞式IO的线程的大部分时间都浪费在等待请求上了。

java NIO常出现的问题:

注册的事件没有及时注销,导致不停的触发.然后cpu100%.

1.读事件没有注销:当客户端关闭连接的时候,channel.read(buf)的时候返回<=0的值,并且会不停触发读事件.这个时候如果没有把当前事件cancel掉,则会出问题.这里很容易同多次读包混淆在一起,当客户端断开时,除了read其它地方都不能判断出客户端已经断开.

2.写事件没有及时注销:当事件可以写完后,要立即把关注写事件给关掉,不然当网卡IO可写的时候就会触发事件.

转自:http://blog.csdn.net/kangojian/article/details/5711027

下面看一下图的对比:

现在我们就用实例来学习下NIO

以下摘自:JAVA编程思想第四版

1.Channel通道:
通道表示实体,如硬件设备、文件、网络套接字或可以执行的一个或多个不同的I/O操作(如读取或写入)的程序组件的开发的链接,用于I/O操作的链接。
通道可处于打开或关闭状态。创建通道时它处于打开状态,一旦将其关闭,则保持关闭状态。一旦关闭了某个通道,试图对其调用I/O操作都会抛出ClosedChannelException异常,通过调用通道的isOpen()方法可以探测通道是否处于打开状态。一般情况下通道对于多线程的访问是安全的。
2.ByteBuffer字节缓冲区:
字节缓冲区是nio中唯一直接和通道channel打交道的缓冲区。字节缓冲区可以存放原始字节,也可以存放java的8中基本类型数据,但是不能存放引用类型数据,String也是不能存放的。正是由于这种底层的存放类型似的字节缓冲区可以更加高效和绝大部分操作系统的I/O进行映射。
字节缓冲区通过allocation()方法创建,此方法为该缓冲区的内容分配空间,或者通过wrapping方法将现有的字节数组包装到缓冲区中来创建。
字节缓冲区的常用操作:
(1).读写单个字节的绝对和相对get和put方法:
a. 绝对方法:
get(int index):读取指定索引处的字节。
put(int index, byte b):将字节写入指定索引处。
b.相对方法:
get():读取此缓冲区当前位置的字节,然后该位置递增。
put(byte b):将字节写入此缓冲区的当前位置,然后该位置递增。
(2).相对批量get方法:
ByteBuffer get(byte[] dst):将此缓冲区的字节传输到给定的目标数组中。
(3).相对批量put方法:
ByteBuffer put(byte[] src):将给定的源byte数组的所有内容传输到此缓冲区中。
(4).读写其他基本类型值:
getChar(), putChar(char value), getChare(int index), putChar(int index, char value).
getInt(), putInt(int value), getInt(int index), putInt(int index, int value)等等读写基本类型值得相对和绝对方法。
注意:基本类型值得相对和绝对读写方法,根据java基本类型数据底层字节数进行缓冲区移动。
(5).创建视图缓冲区:
为了访问同类二进制数据,允许将字节缓冲区视为包含它们基本类型值的缓冲区,视图缓冲区只是其内容受该字节缓冲区支持的另一种缓冲区。字节缓冲区和视图缓冲区内容更改是相互可见的。这两种缓冲区的位置、限制和标记值都是独立的。创建方法如下:
asCharBuffer(), asDoubleBuffer(), asFloatBuffer(), asIntBuffer(), asLongBuffer(), asReadOnlyBuffer(), asShortBuffer()。
与具体类型的get和put方法系列相比,视图缓冲区优势如下:
a视图缓冲区不是根据字节进行索引,而是根据其特定类型的值得大小进行索引。
b.视图缓冲区提供了相对批量get和put方法,这些方法可以在缓冲区和数组或相同类型的其他缓冲区直接传输连续序列的值。
c.视图缓冲区可能更搞效,因为当且仅当其支持的字节缓冲区为直接缓冲区时它才是直接缓冲区。
(6).缓冲区其他操作:
a.ByteBuffer compact():压缩缓冲区,从缓冲区写入数据之后调用,以防写入不完整。
b.ByteBuffer duplicate():创建共享此缓冲区内容的新的字节缓冲区,新缓冲区中的内容为此缓冲区的内容,此缓冲区和新缓冲区内容更改是相互可见的。
c.ByteBuffer slice():创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。
3.直接与非直接缓冲区:
字节缓冲区要么是直接的,要么是非直接的。
如果字节缓冲区是直接的,则JVM会尽最大努力直接在此缓冲区上执行本机的I/O操作,即在每次调用底层操作系统的一个本机I/O之前(或之后),JVM都会尽量避免将缓冲区的内容复制到中间缓冲区中(或尽量避免从中间缓冲区复制内容)。直接字节缓冲区可以通过调用allocateDirect()方法来创建,此方法返回的缓冲区进行分配和取消分配所需的成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此它对应用程序的内存需求量造成的影响可能并不明显,所以建议将直接缓冲区主要分配给那些容易受底层操作系统的本机I/O操作影响的大型的、持久的缓冲区
可以通过调用isDirect()来判断字节缓冲区是直接的还是非直接的。

实例一:

  1. private static final int BSIZE = 1024;  
  2.     public static void main(String[] args) throws Exception{  
  3.         FileChannel ou = new FileOutputStream("D://StreamTest.txt").getChannel() ;  
  4.         ByteBuffer bf = ByteBuffer.allocate(BSIZE);  
  5.         bf.clear();  
  6.         ou.write(ByteBuffer.wrap("StreamTest 测试啊!".getBytes()));  
  7.   
  8.         FileChannel fc = new RandomAccessFile("D://StreamTest.txt","rw").getChannel();  
  9.         ou.write(ByteBuffer.wrap("接着测试啊!".getBytes()));  
  10.         ou.close();  
  11.   
  12.         FileChannel io = new FileInputStream("D://StreamTest.txt").getChannel();  
  13.         bf.clear();  
  14.         io.read(bf);  
  15.         bf.flip();  
  16.         io.close();  
  17.         while(bf.hasRemaining()){  
  18.             System.out.print((char) bf.get());  
  19.         }  
  20.     }  
private static final int BSIZE = 1024;
    public static void main(String[] args) throws Exception{
        FileChannel ou = new FileOutputStream("D://StreamTest.txt").getChannel() ;
        ByteBuffer bf = ByteBuffer.allocate(BSIZE);
        bf.clear();
        ou.write(ByteBuffer.wrap("StreamTest 测试啊!".getBytes()));

        FileChannel fc = new RandomAccessFile("D://StreamTest.txt","rw").getChannel();
        ou.write(ByteBuffer.wrap("接着测试啊!".getBytes()));
        ou.close();

        FileChannel io = new FileInputStream("D://StreamTest.txt").getChannel();
        bf.clear();
        io.read(bf);
        bf.flip();
        io.close();
        while(bf.hasRemaining()){
            System.out.print((char) bf.get());
        }
    }

上面的实例中我们用OutputStream和InputStream生成了文件通道实例。

我们通过ByteBuffer的allocate来创建字符串缓存区。另外还有一个allocateDirect方法,他们有什么区别呢

allocate
public static ByteBuffer allocate(int capacity)分配一个新的字节缓冲区。 
新缓冲区的位置将为零,其界限将为其容量,其标记是不确定的。它将具有一个底层实现数组,且其 数组偏移量将为零。 
参数:
capacity - 新缓冲区的容量,以字节为单位 

allocate和allocateDirect方法都做了相同的工作,不同的是allocateDirect方法直接使用操作系统来分配Buffer。因而它将提供更快的访问速度。不幸的是,并非所有的虚拟机都支持这种直接分配的方法。
Sun推荐将以字节为单位的直接型缓冲区allocateDirect用于与大型文件相关并具有较长生命周期的缓冲区
为什么要提供两种方式呢?这与Java的内存使用机制有关。第一种分配方式产生的内存开销是在JVM中的,而第二种的分配方式产生的开销在JVM之外,以就是系统级的内存分配。当Java程序接收到外部传来的数据时,首先是被系统内存所获取,然后在由系统内存复制拷贝到JVM内存中供Java程序使用。
所以在第二种分配方式中,可以省去复制这一步操作,效率上会有所提高。但是系统级内存的分配比起JVM内存的分配要耗时得多,所以并不是任何时候allocateDirect的操作效率都是最高的。下面是一个不同容量情况下两种分配方式的操作时间对比:

转自:http://hyleeon.iteye.com/blog/1148938


RandomAccessFile  请参考:http://www.cnblogs.com/kelin1314/archive/2010/08/05/1793320.html

所以是实例一的过程是先往文件通道中写入内容,然后使用RandomAccessFile像文件中追加内容,最后读取。

猜你喜欢

转载自mxdxm.iteye.com/blog/2047640
今日推荐