Java中IO与NIO的区别和使用场景


  这几天主要学习了 NIO ,因为之前对 IO 使用的也比较多,所以有一个简单的对比,并且把学习的成果记录下来。

 

java.NIO包里包括三个基本的组件

l buffer:因为NIO是基于缓冲的,所以buffer是最底层的必要类,这也是IO和NIO的根本不同,虽然stream等有buffer开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而NIO却是直接读到buffer中进行操作。

因为读取的都是字节,所以在操作文字时,要用charset类进行编解码操作。

l channel:类似于IO的stream,但是不同的是除了FileChannel,其他的channel都能以非阻塞状态运行。FileChannel执行的是文件的操作,可以直接DMA操作内存而不依赖于CPU。其他比如socketchannel就可以在数据准备好时才进行调用。

l selector:用于分发请求到不同的channel,这样才能确保channel不处于阻塞状态就可以收发消息。

面向流与面向缓冲

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

 

补充一点:NIO的buffer可以使用直接内存缓冲区,该缓冲区不在JVM中,性能会比JVM的缓冲区略好,不过会增加相应的垃圾回收的负担,因为JVM缓冲区的性能已经足够好,所以除非在对缓冲有特别要求的地方使用直接缓冲区,尽量使用JVM缓冲。

阻塞与非阻塞

Java IO是阻塞式的操作,当一个inputstream或outputstream在进行read()或write()操作时,是一直处于等待状态的,直到有数据读/写入后才进行处理.而NIO是非阻塞式的,当进行读写操作时,只会返回当前已经准备好的数据,没有就返回空,这样当前线程就可以处理其他的事情,提高了资源的使用率.

与传统IO的优势

在老的IO包中,serverSocket和socket都是阻塞式的,因此一旦有大规模的并发行为,而每一个访问都会开启一个新线程。这时会有大规模的线程上下文切换操作(因为都在等待,所以资源全都被已有的线程吃掉了),这时无论是等待的线程还是正在处理的线程,响应率都会下降,并且会影响新的线程。

 

而NIO包中的serverSocket和socket就不是这样,只要注册到一个selector中,当有数据放入通道的时候,selector就会得知哪些channel就绪,这时就可以做响应的处理,这样服务端只有一个线程就可以处理大部分情况(当然有些持续性操作,比如上传下载一个大文件,用NIO的方式不会比IO好)。

 

 

通过两个图的比较,可以看出IO是直连的,每个请求都给一条线程来处理,但是NIO却是基于反应堆(selector)来处理,直到读写的数据准备好后,才会通知相应的线程来进行处理。一言以蔽之:“selector不会让channel白占资源,没事的时候给我去睡觉。”

PS:NIO基于字节进行传输,在IO时要注意decode/encode。

 

这里贴出一个基于Tcp的通信操作,体现出了NIO在处理小字节片的优势,比如用一个服务端程序就可以处理所有的客户端请求:

 

TCPProtocol接口

/**

 * TCPServerSelector与特定协议间通信的接口

 *

 * @date    2010-2-3

 * @time    上午08:42:42

 * @version 1.00

 */

publicinterface TCPProtocol{

/**

   * 接收一个SocketChannel的处理

   * @param key

   * @throws IOException

   */

void handleAccept(SelectionKey key) throws IOException;

 

/**

   * 从一个SocketChannel读取信息的处理

   * @param key

   * @throws IOException

   */

void handleRead(SelectionKey key) throws IOException;

 

/**

   * 向一个SocketChannel写入信息的处理

   * @param key

   * @throws IOException

   */

void handleWrite(SelectionKey key) throws IOException;

}

 

TCPProtocol实现类

/**

 * TCPProtocol的实现类

 *

 * @date    2010-2-3

 * @time    上午08:58:59

 * @version 1.00

 */

publicclass TCPProtocolImpl implements TCPProtocol{

privateintbufferSize;

 

public TCPProtocolImpl(intbufferSize){

this.bufferSize=bufferSize;

  }

 

publicvoid handleAccept(SelectionKey key) throws IOException {

    SocketChannel clientChannel=((ServerSocketChannel)key.channel()).accept();

clientChannel.configureBlocking(false);

clientChannel.register(key.selector(),SelectionKey.OP_READ,ByteBuffer.allocate(bufferSize));

  }

 

publicvoid handleRead(SelectionKey key) throws IOException {

// 获得与客户端通信的信道

    SocketChannel clientChannel=(SocketChannel)key.channel();

 

// 得到并清空缓冲区

    ByteBuffer buffer=(ByteBuffer)key.attachment();

buffer.clear();

 

// 读取信息获得读取的字节数

longbytesRead=clientChannel.read(buffer);

 

if(bytesRead==-1){

// 没有读取到内容的情况

clientChannel.close();

    }

else{

// 将缓冲区准备为数据传出状态

buffer.flip();

 

// 将字节转化为为UTF-16的字符串  

      String receivedString=Charset.forName(“UTF-16”).newDecoder().decode(buffer).toString();

 

// 控制台打印出来

      System.out.println(”接收到来自”+clientChannel.socket().getRemoteSocketAddress()+”的信息:”+receivedString);

 

// 准备发送的文本

      String sendString=”你好,客户端. @”+newDate().toString()+”,已经收到你的信息”+receivedString;

buffer=ByteBuffer.wrap(sendString.getBytes(“UTF-16”));

clientChannel.write(buffer);

 

// 设置为下一次读取或是写入做准备

key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

    }

  }

 

publicvoid handleWrite(SelectionKey key) throws IOException {

// do nothing

  }

}

 

服务器

/**

 * TCP服务器端

 *

 * @date    2010-2-3

 * @time    上午08:39:48

 * @version 1.00

 */

publicclass TCPServer{

// 缓冲区大小

privatestaticfinalintBufferSize=1024;

 

// 超时时间,单位毫秒

privatestaticfinalintTimeOut=3000;

 

// 本地监听端口

privatestaticfinalintListenPort=1978;

 

publicstaticvoid main(String[] args) throws IOException{

// 创建选择器

    Selector selector=Selector.open();

 

// 打开监听信道

    ServerSocketChannel listenerChannel=ServerSocketChannel.open();

 

// 与本地端口绑定

listenerChannel.socket().bind(new InetSocketAddress(ListenPort));

 

// 设置为非阻塞模式

listenerChannel.configureBlocking(false);

 

// 将选择器绑定到监听信道,只有非阻塞信道才可以注册选择器.并在注册过程中指出该信道可以进行Accept操作

listenerChannel.register(selector, SelectionKey.OP_ACCEPT);

 

// 创建一个处理协议的实现类,由它来具体操作

    TCPProtocol protocol=new TCPProtocolImpl(BufferSize);

 

// 反复循环,等待IO

while(true){

// 等待某信道就绪(或超时)

if(selector.select(TimeOut)==0){

        System.out.print(”独自等待.”);

continue;

      }

 

// 取得迭代器.selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey

      Iterator<SelectionKey>keyIter=selector.selectedKeys().iterator();

 

while(keyIter.hasNext()){

        SelectionKey key=keyIter.next();

 

try{

if(key.isAcceptable()){

// 有客户端连接请求时

protocol.handleAccept(key);

          }

 

if(key.isReadable()){

// 从客户端读取数据

protocol.handleRead(key);

          }

 

if(key.isValid() &&key.isWritable()){

// 客户端可写时

protocol.handleWrite(key);

          }

        }

catch(IOException ex){

// 出现IO异常(如客户端断开连接)时移除处理过的键

keyIter.remove();

continue;

        }

 

// 移除处理过的键

keyIter.remove();

      }

    }

  }

}

 

 

TCPClient客户端程序

/**

 * NIO TCP 客户端

 *

 * @date    2010-2-3

 * @time    下午03:33:26

 * @version 1.00

 */

publicclass TCPClient{

// 信道选择器

private Selector selector;

 

// 与服务器通信的信道

  SocketChannel socketChannel;

 

// 要连接的服务器Ip地址

private String hostIp;

 

// 要连接的远程服务器在监听的端口

privateinthostListenningPort;

 

/**

   * 构造函数

   * @param HostIp

   * @param HostListenningPort

   * @throws IOException

   */

public TCPClient(String HostIp,intHostListenningPort)throws IOException{

this.hostIp=HostIp;

this.hostListenningPort=HostListenningPort;  

 

    initialize();

  }

 

/**

   * 初始化

   * @throws IOException

   */

privatevoid initialize() throws IOException{

// 打开监听信道并设置为非阻塞模式

socketChannel=SocketChannel.open(new InetSocketAddress(hostIp, hostListenningPort));

socketChannel.configureBlocking(false);

 

// 打开并注册选择器到信道

selector = Selector.open();

socketChannel.register(selector, SelectionKey.OP_READ);

 

// 启动读取线程

new TCPClientReadThread(selector);

  }

 

/**

   * 发送字符串到服务器

   * @param message

   * @throws IOException

   */

publicvoid sendMsg(String message) throws IOException{

    ByteBuffer writeBuffer=ByteBuffer.wrap(message.getBytes(“UTF-16”));

socketChannel.write(writeBuffer);

  }

 

publicstaticvoid main(String[] args) throws IOException{

    TCPClient client=new TCPClient(“127.0.0.1”,1978);

    String str = “”;

for(inti=0; i<1000; i++) {

    str+=i+“,”;

    }

client.sendMsg(str);

  }

}

 

客户端接收线程

public TCPClientReadThread(Selector selector){

this.selector=selector;

 

new Thread(this).start();

  }

 

publicvoid run() {

try {

while (selector.select() > 0) {

// 遍历每个有可用IO操作Channel对应的SelectionKey

for (SelectionKey sk : selector.selectedKeys()) {

 

// 如果该SelectionKey对应的Channel中有可读的数据

if (sk.isReadable()) {

// 使用NIO读取Channel中的数据

            SocketChannel sc = (SocketChannel)sk.channel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);

sc.read(buffer);

buffer.flip();

 

// 将字节转化为为UTF-16的字符串  

            String receivedString=Charset.forName(“UTF-16”).newDecoder().decode(buffer).toString();

 

// 控制台打印出来

            System.out.println(”接收到来自服务器”+sc.socket().getRemoteSocketAddress()+”的信息:”+receivedString);

 

// 为下一次读取作准备

sk.interestOps(SelectionKey.OP_READ);

          }

 

// 删除正在处理的SelectionKey

selector.selectedKeys().remove(sk);

        }

      }

    } catch (IOException ex) {

ex.printStackTrace();

    }  

  }

}

 

 

文件读取的性能

 

我们已知的文件读写包括三种方式:流读写、channel(缓冲读写)、直接内存缓冲区读写。

 

面向流的读写

 

文件映射的方式

 

 

由图可见,文件映射的方式比传统方式不用的是,再也不用磁盘控制器拷贝到内核空间了,只需要根据对物理内存的映射,就能让进程直接操作磁盘了。但是直接内存映射的方式会使用双倍内存,就是说在JVM和物理内存中,他会有两份空间,因此使用的时候可以权衡一下性能。

 

代码示例:

public class FileCopyTest { 

   

    

   

    public  static  void main(String[]args) throws Exception{ 

   

       StringsourcePath = “F:\mywork\javademo\dir1\movie.avi”; 

   

       StringdestPath1 = “F:\mywork\javademo\dir2\movie1.avi”; 

   

       StringdestPath2 = “F:\mywork\javademo\dir2\movie2.avi”; 

   

       StringdestPath3 = “F:\mywork\javademo\dir2\movie3.avi”; 

   

       long t1 = System.currentTimeMillis(); 

   

       traditionalCopy(sourcePath,destPath1); 

   

       long t2 = System.currentTimeMillis(); 

   

       System.out.println(“传统IO方法实现文件拷贝耗时:” + (t2-t1) + “ms”); 

   

         

   

       nioCopy(sourcePath,destPath2); 

   

       long t3 = System.currentTimeMillis(); 

   

       System.out.println(“利用NIO文件通道方法实现文件拷贝耗时:” + (t3-t2) + “ms”); 

   

         

   

       nioCopy2(sourcePath,destPath3); 

   

       long t4 = System.currentTimeMillis(); 

   

       System.out.println(“利用NIO文件内存映射及文件通道实现文件拷贝耗时:” + (t4-t3) + “ms”); 

   

         

   

    } 

   

    

   

    private  static  void nioCopy2(StringsourcePath, String destPath) throws Exception { 

   

       File source = new File(sourcePath); 

   

       File dest = new File(destPath); 

   

       if(!dest.exists()){ 

   

           dest.createNewFile(); 

   

       } 

   

       FileInputStreamfis = new FileInputStream(source); 

   

       FileOutputStreamfos = new FileOutputStream(dest); 

   

       FileChannelsourceCh = fis.getChannel(); 

   

       FileChanneldestCh = fos.getChannel(); 

   

       MappedByteBuffermbb = sourceCh.map(FileChannel.MapMode.READ_ONLY, 0, sourceCh.size()); 

   

       destCh.write(mbb); 

   

       sourceCh.close(); 

   

       destCh.close(); 

   

    } 

   

    

   

    

   

    private  static  void traditionalCopy(StringsourcePath, String destPath) throws Exception{ 

   

       File source = new File(sourcePath); 

   

       File dest = new File(destPath); 

   

       if(!dest.exists()){ 

   

           dest.createNewFile(); 

   

       } 

   

       FileInputStreamfis = new FileInputStream(source); 

   

       FileOutputStreamfos = new FileOutputStream(dest); 

   

       byte [] buf = newbyte [512]; 

   

       int len = 0; 

   

       while((len =fis.read(buf)) != -1) { 

   

           fos.write(buf, 0, len); 

   

       } 

   

       fis.close(); 

   

       fos.close(); 

   

    } 

   

    

   

    private  static  void nioCopy(StringsourcePath, String destPath) throws Exception{ 

   

       File source = new File(sourcePath); 

   

       File dest = new File(destPath); 

   

       if(!dest.exists()){ 

   

           dest.createNewFile(); 

   

       } 

   

       FileInputStreamfis = new FileInputStream(source); 

   

       FileOutputStreamfos = new FileOutputStream(dest); 

   

       FileChannelsourceCh = fis.getChannel(); 

   

       FileChanneldestCh = fos.getChannel(); 

   

       destCh.transferFrom(sourceCh, 0, sourceCh.size()); 

   

       sourceCh.close(); 

   

       destCh.close(); 

   

    } 

   

  每执行完一次拷贝之后,将F:myworkjavademodir2目录中的内容删除掉,重复执行8次。观察测试结果如下:时间单位为ms(毫秒)

          

         由上表可知,传统IO方式平均拷贝完成时间约为1968ms,NIO文件通道方式平均拷贝完成时间约为1672ms,文件内存映射及文件通道方式平均拷贝完成时间约为1418ms。

 

 

文章参考自:

http://www.open-open.com/lib/view/open1413518521372.html

http://weixiaolu.iteye.com/blog/1479656




评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值