Java NIO详解

I/O阻塞和NIO非阻塞

传统Java IO是基于阻塞I/O模式,这意味着,当一个线程在调用read()和write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,该线程在此期间不能再干任何事情,不仅如此,传统的输入流,输出流都是通过字节的移动来处理的(即使不直接去处理字节流,但底层的实现还是依赖于字节处理),Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅仅能得到目前可用的数据,如果目前没有数据可用,就什么都不获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做任何事情,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情,线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道。

NIO

NIO包括三个核心概念:缓冲区(Buffer),通道(CHannel),选择器(Selector),思维导图如下:
image

Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输:Channel与传统的InputStream,OutputStream最大的区别在于它提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出系统是面向流的处理,则新IO则是面向块的处理。

Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。

除了Channel和Buffer之外,新IO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入/输出的Selector类,用于检查一个或多个NIO Channel(通道)的状态是否处于可读,可写。如此可以实现单线程管理多个Channels,也就是可以管理多个网络链接。

Buffer

一个Buffer对象是固定数量的数据容量,其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索。

在Buffer中有三个重要的概念:容量(capacity),界限(limit),和位置(position).

1.容量(capacity):缓冲区的容量(capacity)表示该Buffer的最大数据容量,即最多可以存储多少数据,缓冲区的容量不可能为负值,创建后不能改变。

2.界限(limit):第一个不应该被读出或者写入的缓冲区位置索引,也就是说,位于limit后的数据既不可被读,也不可被写。

3.位置(position):用于指出下一个可以被读出或者写入的缓冲区位置索引(类似于IO流中的记录指针)。当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。当刚刚新建一个Buffer对象时,其position为0;如果从Channel中读取了2个数据到该Buffer中,则position为2,指向Buffer中第3个(第1个位置的索引为0)位置。
这些值满足如下关系:

0<=mark<=position<=limit<=capacity

image

Buffer中put()和get()

当使用put()和get()方法放入、取出数据时,Buffer既支持对单个数据的访问,也支持对批量数据的访问(以数组作为参数)。

当使用put()和get()来访问Buffer中的数据时,分为相对和绝对两种。

1.相对(Relative):从Buffer的当前position处开始读取或写入数据,然后将位置(position)的值按处理元素的个数增加。

2.绝对(Absolute):直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据时,并不会影响位置(position)的值。

package buffer;
import java.nio.CharBuffer;

public class BufferTest {
    public static void main(String[] args) {
        //创建Buffer
        CharBuffer buff = CharBuffer.allocate(8);
        System.out.println("capacity:"+buff.capacity());//capacity:8
        System.out.println("limit:"+buff.limit());//limit:8
        System.out.println("position:"+buff.position());//position:0

        //放入元素
        buff.put('a');
        buff.put('b');
        buff.put('c');
        System.out.println("加入三个元素后,position的值为:"+buff.position());//加入三个元素后,position的值为:3

        //调用flip()方法
        buff.flip();
        //调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0
        System.out.println("执行flip()方法后:limit的值:"+buff.limit());//执行flip()方法后:limit的值:3
        System.out.println("执行flip()方法后:position的值:"+buff.position());//执行flip()方法后:position的值:0

        //按相对位置取出元素
        System.out.println("按相对位置取出的第一个元素(position=0)"+buff.get());//第一个元素(position=0)a
        System.out.println("按相对位置取出第一个元素后,position的值:"+buff.position());//取出第一个元素后,position的值:1
        System.out.println("按相对位置取出第一个元素后,limit的值:"+buff.limit());//取出第一个元素后,limit的值:3

        //按绝对位置取出元素
        System.out.println("按绝对位置的第一个元素(position=0)"+buff.get(0));//第一个元素(position=0)a
        System.out.println("按绝对位置取出第一个元素后,position的值:"+buff.position());//取出第一个元素后,position的值:1
        System.out.println("按绝对位置取出第一个元素后,limit的值:"+buff.limit());//取出第一个元素后,limit的值:3

        //调用clear()方法
        buff.clear();
        //Buffer调用clear()方法,clear()方法不是清空Buffer的数据,它仅仅将position置为0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备
        System.out.println("调用clear()方法后,limit的值:"+buff.limit());//调用clear()方法后,limit的值:8
        System.out.println("调用clear()方法后,position的值:"+buff.position());//调用clear()方法后,position的值:0

        System.out.println("第二个元素(position=0)"+buff.get(1));//第二个元素(position=0)b
        //因为上面代码采用的是根据索引来取值的方式,所以该方法不会影响Buffer的position
        System.out.println("取出第二个元素后,position的值:"+buff.position());//取出第二个元素后,position的值:0
        System.out.println("取出第二个元素后,position的值:"+buff.limit());//取出第二个元素后,position的值:8

    }
}

Channel

Channel类似于传统的流对象,但与传统的流对象有两个主要区别:

1.Channel可以直接将制定文件的部分或全部直接映射成Buffer.

2.程序不能直接访问Channel中的数据,包括读取,写入都不行,Channel只能与Buffer进行交互,也就是说,如果要从Channel中取出数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。

Channel中常用方法

Channel中最常用的三类方法是map(),read()和write(),其中map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer:而read()或write()方法都有一系列重载方式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。

map()方法的方法签名为MappedByteBuffer map(FileChannel.MapMode mode,long position,long size),第一个参数执行映射时的模式,分别有只读,读写等模式;而第二个,第三个参数用于控制将Channel的哪些数据映射成ByteBuffer.

package buffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class FileChannelTest {
    public static void main(String[] args) {

        File f = new File("FileChannelTest.java");

        try(
                // //创建FileInputStream,以该文件输入流创建FileChannel
                FileChannel inChennel = new FileInputStream(f).getChannel();
                //以文件输出流创建FileChannel,用以控制输出.
                FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
        ) {
            //将FileChannel里的数据全部映射成ByteBuffer
            MappedByteBuffer buffer = inChennel.map(FileChannel.MapMode.READ_ONLY,0,f.length());
            //使用GBK的字符集来创建解码器
            Charset  charset = Charset.forName("GBK");
            //直接将buffer里的数据全部输出,即a.txt文件里面的数据变成FileChannelTest.java文件里面的数据
            outChannel.write(buffer);
            //再次调用buffer的clear()方法,复原limit,position的位置
            buffer.clear();
            //创建解码器对象
            CharsetDecoder decoder = charset.newDecoder();
            //使用解码器将ByteBuffer转换成CharBuffer
            CharBuffer charBuffer = decoder.decode(buffer);
            //CharBuffer的toString方法可以获取对应的字符串
            System.out.println(charBuffer);//kk bb hh

        }catch(Exception ex){
            ex.printStackTrace();
        }

    }
}

如果担心Channel对应的文件过大,使用map()方法一次将所有的文件映射到内存中引起性能下降,也可以使用Channel和Buffer传统的“用竹筒多次重复取水”的方式。

package buffer;

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class ReadFile {
    public static void main(String[] args) {
        try(
                //创建文件输入流
                FileInputStream fis = new FileInputStream("ReadFile.java");
                //创建一个FileChannel
                FileChannel fc = fis.getChannel()){
                //定义一个ByteBuffer对象,用于重复取水
                ByteBuffer byteBuffer = ByteBuffer.allocate(256);
                //将FileChannel中的数据放入ByteBuffer中
                 while(fc.read(byteBuffer)!=-1){
                //锁定Buffer的空白区,将数据的区域“封印”起来,避免程序从Buffer中取出null值
                byteBuffer.flip();
                //创建Charset对象
                Charset charset= Charset.forName("GBK");
                //创建解码器对象
                CharsetDecoder decoder = charset.newDecoder();
                //将ByteBuffer的内容转码
                CharBuffer charBuffer = decoder.decode(byteBuffer);
                System.out.println(charBuffer);
                //将Buffer初始化。为下一次读取数据做准备
                byteBuffer.clear();
            }

        }catch(Exception e){
            e.printStackTrace();

        }
    }
}

Selector(选择器)

selector是用来检查一个或多个NIO Channel(通道)的状态是否处于可读,可写,如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。如果不使用Selector,要监听多个channel就需要多线程操作,一个线程监听一个通道的事件,这样导致线程上下文切换的开销(内存),增加了编程的复杂度。

扫描关注下面二维码获得更多有用的资源!
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值