Java NIO - Buffer & Channel

Java NIO(Non-blocking Input/Output)提供了一种新的IO操作方式,以缓冲区(Buffer)和通道(Channel)为核心,支持非阻塞模式,提高程序的并发性能。Buffer用于数据读写,具备capacity、position和limit等关键属性;Channel是双向的,可以从文件、网络读取数据,也可写入。Selector用于监听多个Channel的事件,实现单线程处理多个连接。通过使用NIO,开发者可以创建高效、伸缩性强的网络应用程序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java NIO - Buffer & Channel

Java NIO 为java non-blocking IO,从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,是同步非阻塞

NIO三大核心部分:

  • Channel(通道)
  • Buffer(缓冲区)
  • Selector(选择器)

NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络

Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直到数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情

通俗的理解:NIO是可以做到一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的BIO,非得分配10000个线程

NIO整个流程的示意图如下:
NIO流程示意图

  • 每个channel都对应一个buffer
  • selector会对应一个线程,一个线程对应多个channel
  • 程序切换到哪个channel,是由事件决定的
  • selector会根据不同的事件,在各个通道上切换
  • buffer就是一个内存块,底层是有一个数组的
  • 数据的读取写入,是通过buffer,这个和BIO有本质的不同的
  • BIO中要么是输入流,或者是输出流,不能双向。但是NIO的buffer是可以读也可以写,需要flip方法切换
  • channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的

NIO和BIO的比较
1.BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO高很多
2.BIO是阻塞的,NIO则是非阻塞的
3.BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于去监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听多个客户单通道

Buffer

Buffer(缓冲区):缓冲区本质上是一个可以读写数据的内存块,可以理解为一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel提供了从文件、网络读取数据的渠道,但是读取或写入的数据必须经过Buffer

Buffer
与Java基本类型相对应,NIO提供了多种 Buffer 类型,如ByteBufferCharBufferIntBuffer
Buffer的类的层级如下:
Buffer的子类
Buffer中有4个很重要的变量,它们是理解Buffer工作机制的关键,分别是

  • capacity - 总容量
  • position - 指针当前位置
  • limit - 读/写边界位置
  • mark - 标记

Buffer的工作方式跟C语言里的字符数组非常的像,类比一下,capacity就是数组的总长度,position就是我们读/写字符的下标变量,limit就是结束符的位置。Buffer初始时3个变量的情况如下图

Buffer
如下使用IntBuffer的简单例子

public class BasicBuffer {
    public static void main(String[] args) {
        //创建一个Buffer,大小为5,即可以存放5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);

        //存放数据
//        intBuffer.put(10);
//        intBuffer.put(11);
//        intBuffer.put(12);
//        intBuffer.put(13);
//        intBuffer.put(14);

        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i * 2);
        }

        //取数据
        //将buffer转换,读写切换
        intBuffer.flip();
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }

    }
}

输出结果为:

0
2
4
6
8

打个断点,调试看下:
1.在IntBuffer intBuffer = IntBuffer.allocate(5);之后
01
2.在for循环运行之后
02
3.intBuffer.flip();之后
03
4.读取第一个后
04
所以,也可以通过设置positionlimit来限制读取的范围,在上面例子的基础上,如果设置:

//取数据
//将buffer转换,读写切换
intBuffer.flip();
intBuffer.position(1);
intBuffer.limit(3);

此时输出的结果为(可见,不能超过limit):

2
4

其它方法

1.clear()
clear() - 清除此缓冲区,即将各个标记恢复到初始状态,但是数据并没有真正擦除

2.hasRemaining()
hasRemaining() - 告知此缓冲区是否具有可访问的底层实现数组

3.isReadOnly()
isReadOnly() - 告知此缓冲区是否为只读缓冲区

4.hasArray()
hasArray() - 告知此缓冲区师傅具有可访问的底层实现数组

5.array()
array() - 返回此缓冲区底层实现数组

Channel

NIO的通过类似于流,但有如下的区别

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读取数据,也可以写数据到缓冲

Channel是一个接口

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return {@code true} if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

Java NIO中最常用的通道实现是如下几个,可以看出跟传统的 I/O 操作类是一一对应的。

  • FileChannel:读写文件
  • DatagramChannel: UDP协议网络通信
  • SocketChannel:TCP协议网络通信
  • ServerSocketChannel:监听TCP连接

FileChannel

FileChannel也是一个抽象类,真实实现为FileChannelImpl

FileChannel主要用来对本地文件进行IO操作,常见的方法有:

1.int read(ByteBuffer dst) - 从通道读取数据并放到缓冲区中
2.int write(ByteBuffer src) - 把缓冲区的数据写到通道中
3.long transferFrom(ReadableByteChannel src, long position, long count) - 从目标通道中复制数据到当前通道
4.long transferTo(long position, long count, WritableByteChannel target) - 把数据从当前通道复制给目标通道

本地文件写数据

本地文件写数据的流程,可以用图表示如下:

本地文件写数据

public class NIOFileChannel {
    public static void main(String[] args) throws Exception {
        String str = "Hello, FileChannel";
        //创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("file01.txt");
        //通过fileOutputStream获取对应的channel
        FileChannel channel = fileOutputStream.getChannel();
        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将 str 放入到buffer中
        byteBuffer.put(str.getBytes());
        //byteBuffer进行反转
        byteBuffer.flip();
        //将bytebuffer的数据,写入到fileOutputStream
        channel.write(byteBuffer);
        //关闭
        fileOutputStream.close();
    }
}
本地文件读数据

本地文件读数据的流程如下:
本地文件读数据

public class NIOFileChannel01 {
    public static void main(String[] args) throws Exception {
        File file = new File("file01.txt");
        //创建一个输入流
        FileInputStream fileInputStream = new FileInputStream(file);
        //通过FileInputStream获取对应的channel
        FileChannel channel = fileInputStream.getChannel();
        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //将通道的数据读入到buffer
        channel.read(byteBuffer);
        //将bytebuffer转为string
        System.out.println(new String(byteBuffer.array()));
        //关闭
        fileInputStream.close();
    }
}
使用一个Buffer完成文件的读写

流程如下:
使用一个Buffer完成文件的读写

public class NIOFileChannel03 {
    public static void main(String[] args) throws Exception {
        //创建一个输入流
        FileInputStream fileInputStream = new FileInputStream("file01.txt");
        //通过FileInputStream获取对应的channel
        FileChannel channel01 = fileInputStream.getChannel();

        //创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("file02.txt");
        //通过fileOutputStream获取对应的channel
        FileChannel channel02 = fileOutputStream.getChannel();

        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        while (true) {//循环读取
            byteBuffer.clear();//重置
            int read = channel01.read(byteBuffer);
            if (read == -1) {//表示读完
                break;
            }
            //buffer中的数据放入到channel02
            byteBuffer.flip();
            channel02.write(byteBuffer);
        }

        //关闭
        fileInputStream.close();
        fileOutputStream.close();
    }
}
拷贝文件

使用transferFrom拷贝文件

public class NIOFileChannel04 {
    public static void main(String[] args) throws Exception {
        //创建一个输入流
        FileInputStream fileInputStream = new FileInputStream("avatar.png");
        //通过FileInputStream获取对应的channel
        FileChannel sourceChannel = fileInputStream.getChannel();

        //创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("avatar_copy.png");
        //通过fileOutputStream获取对应的channel
        FileChannel destChannel = fileOutputStream.getChannel();

        //使用transferFrom完成拷贝
        destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());

        //关闭
        sourceChannel.close();
        destChannel.close();
        fileInputStream.close();
        fileOutputStream.close();
    }
}

Buffer和Channel的注意事项和细节

1.ByteBuffer支持类型化的put和get,put放入的是什么类型,get就应该使用相应的类型来取出,否则可能会有BufferUnderflowException异常

如下的例子,没有按放入的类型取数据,抛出了异常:

public class NIOFileChannel05 {
    public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate(64);
        buffer.putInt(100);
        buffer.putLong(9);
        buffer.putChar('王');
        buffer.putShort((short) 4);

        //取出
        buffer.flip();
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getLong());
    }
}

异常

2.可以将一个普通的Buffer转为只读Buffer

如下例子,对一个只读的Buffer,写入数据,会抛出异常

public class NIOFileChannel06 {
    public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate(64);
        for (int i = 0; i < 64; i++) {
            buffer.put((byte) i);
        }
        buffer.flip();
        //得到一个只读的buffer
        ByteBuffer readonlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readonlyBuffer.getClass()); //java.nio.HeapByteBufferR

        //读取
        while (readonlyBuffer.hasRemaining()) {
            System.out.println(readonlyBuffer.get());
        }

        //readonlyBuffer.put((byte) 1); //抛出异常
    }
}

异常如下:
异常

3.MappedByteBuffer

MappedByteBuffer可以让文件直接在内存(堆外的内存)中进行修改,操作系统不需要拷贝一次,同步到文件则是由NIO来完成。

MappedByteBuffer继承自ByteBuffer

如下的例子:

public class MappedByteBufferDemo  {
    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("file01.txt", "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        /**
         * 参数1: 使用的读写模式
         * 参数2:修改的起始位置
         * 参数3:映射到内存的大小,即多个字节映射到内存,即可以修改的范围
         */
        MappedByteBuffer map = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        map.put(0, (byte) '1');
        map.put(1, (byte) '2');
    }
}

原文件内容为Hello, FileChannel,修改后,变成如下的形式:
MappedByteBuffer

Scattering & Gathering

前面的例子,都是通过一个Buffer完成的,NIO还支持多个Buffer(即Buffer数组)完成读写操作,即ScatteringGathering

Scattering - 将数据写入到Buffer时,可以采用buffer数组,依次写入(分散)
Gathering - 从Buffer读取数据时,可以采用buffer素组,依次读入(聚合)

如下使用Buffer数组的例子:

public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        //绑定端口到socket并启动
        serverSocketChannel.socket().bind(inetSocketAddress);

        //创建Buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);

        //等待客户端连接
        SocketChannel socketChannel = serverSocketChannel.accept();
        int messageLength = 8;//从客户端接收8个字节

        //循环读取
        while (true) {
            int byteRead = 0;
            while (byteRead < messageLength) {
                long read = socketChannel.read(byteBuffers);
                byteRead += read;
                System.out.println("byteRead = " + byteRead);
                //流打印,看当前buffer的position 和 limit
                Arrays.asList(byteBuffers).stream()
                        .map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit())
                        .forEach(System.out::println);

                //将所有的buffer进行flip
                Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());

                //将数据读出显示到客户端
                long byteWrite = 0;
                while (byteWrite < messageLength) {
                    long write = socketChannel.write(byteBuffers);
                    byteWrite += write;
                }

                //将所有的buffer clear
                Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());

                System.out.println("byteWrite = " + byteWrite);
            }
        }


    }
}

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值