美文网首页
Java NIO实现原理之Channel

Java NIO实现原理之Channel

作者: Monica2333 | 来源:发表于2018-10-27 13:56 被阅读0次

    Channel类似与流,通道的特点:
    既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
    通道可以异步地读写。
    通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
    channel的类结构图如下:

    channel类结构.png
    其中:
    AbstractInterruptibleChannel:NIO中可中断channel的基本实现,可参考
    Java NIO中线程的中断机制
    ReadableByteChannel,WritableByteChannel,SelectableChannel,NetworkChannel这些接口或类都是字如其名,分别提供了io传输中的功能,如read,write。。
    具体的channel实现类常见的如下:
    FileChannel: 从文件中读写数据。
    DatagramChannel:能通过UDP读写网络中的数据。
    SocketChannel: 能通过TCP读写网络中的数据。
    ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
    简单的FileChannel用法为:
    public class FileChannelText {
        public static void main(String args[]) throws IOException {
            //1.创建一个RandomAccessFile(随机访问文件)对象
            RandomAccessFile raf=new RandomAccessFile("D:\\aaa.txt", "rw");
            //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。
            FileChannel inChannel=raf.getChannel();
            //2.创建一个读数据缓冲区对象
            ByteBuffer buf=ByteBuffer.allocate(48);
            //3.从通道中读取数据
            int bytesRead = inChannel.read(buf);
            //创建一个写数据缓冲区对象
            ByteBuffer buf2=ByteBuffer.allocate(48);
            //写入数据
            buf2.put("filechannel test".getBytes());
            buf2.flip();
            inChannel.write(buf);
            while (bytesRead != -1) {
    
                System.out.println("Read " + bytesRead);
                //Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
                buf.flip();
               //如果还有未读内容
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get());
                }
                //清空缓存区
                buf.clear();
                bytesRead = inChannel.read(buf);
            }
            //关闭RandomAccessFile(随机访问文件)对象
            raf.close();
        }
    }
    

    其中FileChannel的.read实现如下:

    public int read(ByteBuffer dst) throws IOException {
      //确保channel是打开状态
        ensureOpen();
        if (!readable)
            throw new NonReadableChannelException();
        synchronized (positionLock) {
            int n = 0;
            int ti = -1;
            try {
                begin();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    n = IOUtil.read(fd, dst, -1, nd);
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                end(n > 0);
                assert IOStatus.check(n);
            }
        }
    }
    

    可以看到读取数据逻辑是 n = IOUtil.read(fd, dst, -1, nd);
    IOUtil.read(fd, dst, -1, nd)的实现:

    static int read(FileDescriptor fd, ByteBuffer dst, long position,
                    NativeDispatcher nd) IOException {
        if (dst.isReadOnly())
            throw new IllegalArgumentException("Read-only buffer");
        if (dst instanceof DirectBuffer)
            return readIntoNativeBuffer(fd, dst, position, nd);
    
        // Substitute a native buffer
        ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
        try {
            int n = readIntoNativeBuffer(fd, bb, position, nd);
            bb.flip();
            if (n > 0)
                dst.put(bb);
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }
    

    如果ByteBuffer dst是DirectBuffer,则直接将数据从内核复制到dst,如果是堆内内存,则需要先创建一个临时DirectBuffer bb,将数据拷贝到bb之后,再将数据拷贝到ByteBuffer dst中,经过了两次数据复制。
    Q:那为什么操作系统不直接访问Java堆内的内存区域了?
    A:这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地址,那么该内存地址指向的是Java堆内内存的话,那么如果在操作系统正在访问这个内存地址的时候,Java在这个时候进行了GC操作,而 GC操作 会涉及到数据的移动操作,GC经常会进行先标志在压缩的操作。即,将可回收的空间做标志,然后清空标志位置的内存,然后会进行一个压缩,压缩就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。

    同样,write实现堆内内存也经过了两次复制。
    FIleChannelimpl.write

    ublic int write(ByteBuffer src) throws IOException {
        ensureOpen();
        if (!writable)
            throw new NonWritableChannelException();
        synchronized (positionLock) {
            int n = 0;
            int ti = -1;
            try {
                begin();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    n = IOUtil.write(fd, src, -1, nd);
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                end(n > 0);
                assert IOStatus.check(n);
            }
        }
    }
    

    IOUtil.write(fd, src, -1, nd)的实现:

    static int write(FileDescriptor fd, ByteBuffer src, long position,
                     NativeDispatcher nd) throws IOException {
        if (src instanceof DirectBuffer)
            return writeFromNativeBuffer(fd, src, position, nd);
        // Substitute a native buffer
        int pos = src.position();
        int lim = src.limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
        try {
            bb.put(src);
            bb.flip();
            // Do not update src until we see how many bytes were written
            src.position(pos);
            int n = writeFromNativeBuffer(fd, bb, position, nd);
            if (n > 0) {
                // now update src
                src.position(pos + n);
            }
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }
    

    参考资料:
    https://www.jianshu.com/p/052035037297
    https://www.jianshu.com/p/007052ee3773
    http://ifeve.com/channels/
    https://segmentfault.com/a/1190000014869494

    相关文章

      网友评论

          本文标题:Java NIO实现原理之Channel

          本文链接:https://www.haomeiwen.com/subject/mosltqtx.html