java-nio-1

209 阅读5分钟

NIO

IO多路复用

  • selector(基于轮询); JDK的IO复用使用,支持FD_SIZE = 1024
  • poll(基于轮询),对selector修改没有上限
  • epoll(基于信号量的)
  • Kqueue:mac

IO

也叫BIO,阻塞IO.

  • JDK支持 import java.io.*; 采用的设计模式是 装饰者(Decorator)
  • 基于流的数据类型做分类
    • 字符流:Reader,Writer;java使用unicode编码,一个字符2个字节
    • 字节流:InputStream, OutputStream

java-io.png

内核进程/用户进程切换:

内核将数据复制到用户进程空间(内核进程/用户进程切换:内核进程占用CPU并读取数据到用户进程空间,用户进程停止阻塞不再获取CPU)

NIO

同步非阻塞

  • 三大组件:Buffer,Channel,Selector; 下文有关于三大组件的使用介绍
  • Channel + Buffer: 解决了非阻塞问题(这个说法有待商榷...错的,应该是selector,channel + buffer提供了高效的IO)
  • Selector是同步的,实现单线程管理多个channel,替换了多线程模式;解决了阻塞的问题
  • JDK支持:java.nio.*; 应用netty
  • selector管理Channel的IO事件,

java-nio.jpeg

  • FileChannel 不支持非阻塞。。。。(还需要继续调研)
// tomcat配置NIO支持: 线程池模型 => selector + buffer + 线程池(处理业务)
Connector 的 protocol="org.apache.coyote.http11.Http11NioProtocol"

AIO

异步IO

  • 异步通知(需要系统的信号量支持)
  • 增加了Future和回调函数,例如Future/Promise,CompletableFuture;CompletionHandler

总共有三个类需要我们关注,分别是 AsynchronousSocketChannel,AsynchronousServerSocketChannel 和 AsynchronousFileChannel,只不过是在之前介绍的 FileChannel、SocketChannel 和 ServerSocketChannel 的类名上加了个前缀 Asynchronous

思维导图&反思

只有反思过,见到/听到的东西才是自己的东西

IO与NIO区别

  • IO阻塞, NIO非阻塞
  • IO基于流(字节流 + 字符流); NIO基于通道和缓冲区(channel + buffer + Selector)

2.阻塞-非阻塞和同步-异步

  • 阻塞/非阻塞:针对IO来说的, 指当前进程是否等待IO完成;通过selector实现,
  • 同步/异步:针对进程来说,指进程内的多个子任务是否串行执行;通过多线程实现;如果需要返回值,可以通过回调/异步查询的方式。

3.解释内核进程与用户进程的切换

IO为例

技术文档

Buffer

Buffer是在机器内存上分配的空间,不在JVM的空间中;这段空间的使用类似C程序的共享内存(C语言共享内存实现进程的通信一样);特别注意的是内存的溢出和内存的释放

简单的类图: Buffer <-继承 ByteBuffer <-继承 MappedByteBuffer ; 尬...Buffer是最上层的abstract类,ByteBuffer是重要的实现,MappedByteBuffer是内存映射文件(在ROcketMQ源码中有使用); ByteBuffer同等级的实现还有IntBuffer,CharBuffer.....

// Buffer的源码
public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    // 当前读/写指针的位置
    private int position = 0;
    // 写模式下,可写的空间
    private int limit;
    // 内存空间的大小
    private int capacity;
    /*
     * buf.put(magic);    // 写 header
     * in.read(buf);      // 从channel 读书数据到buf
     * buf.flip();        // 切换模式
     * out.write(buf);    // 从buf 读书到 out
     * buffer从写模式 切换到 读模式
     */
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    // 使用mark标记pos当前位置
    public final Buffer mark() {
        mark = position;
        return this;
    }
    // 恢复pos到上一次mark的位置
    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }
    // 重置pos
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    // 清空指针
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
}

jdk8引入的直接内存,和堆内存空间, 跟netty的分配很像

Channel

统一封装用户IO操作,便于selector管理,完成了BIO中流(stream)的作用。

// 这里需要从面向对象的角度去读
FileChannel channel = file.getChannel();
ByteBuffer buf =  ByteBuffer.allocate(10);
channel.read(buf); //  read channel into buf
buf.flip();
channel.write(buf); // write channel from buf

FileChannel 不支持非阻塞。。。这个是在其他博文看到的,还没有测试

Selector

selector实现单线程管理多个channel,通过循环校验代替了阻塞;

// Selector的范式
public static void testSelector() {
        try {
            // TCP Server
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            // 默认=true是阻塞的
            // 只能注册非阻塞的事件
            serverSocketChannel.configureBlocking(false);

            // 获取Selector
            Selector selector = Selector.open();
            // 注册监听时间
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                // 判断是否有事件准备好
                int readyChannels = selector.select();
                if (readyChannels == 0) {
                    continue;
                }
                // 遍历处理;此处使用的循环校验; 不是阻塞
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                // 处理准备好的事件
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    if (key.isAcceptable()) {
                        // a connection was accepted by a ServerSocketChannel.
                    } else if (key.isConnectable()) {
                        // a connection was established with a remote server.
                    } else if (key.isReadable()) {
                        // a channel is ready for reading
                    } else if (key.isWritable()) {
                        // a channel is ready for writing
                    }
                    keyIterator.remove();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

CompletionHandler

回调函数

public interface CompletionHandler<V,A> {
    // A attchment是个透传参数
    void completed(V result, A attachment);
    void failed(Throwable exc, A attachment);
}

AsynchronousFileChannel

异步文件IO

首先,我们就来关注异步的文件 IO,前面我们说了,文件 IO 在所有的操作系统中都不支持非阻塞模式,但是我们可以对文件 IO 采用异步的方式来提高性能,下面,我会介绍 AsynchronousFileChannel 里面的一些重要的接口,都很简单,读者要是觉得无趣,直接滑到下一个标题就可以了。

实例化:

AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("/Users/hongjie/test.txt"));

一旦实例化完成,我们就可以着手准备将数据读入到 Buffer 中:

ByteBuffer buffer = ByteBuffer.allocate(1024);
Future<Integer> result = channel.read(buffer, 0);

异步文件通道的读操作和写操作都需要提供一个文件的开始位置,文件开始位置为 0

除了使用返回 Future 实例的方式,也可以采用回调函数进行操作,接口如下:

public abstract <A> void read(ByteBuffer dst,
                              long position,
                              A attachment,
                              CompletionHandler<Integer,? super A> handler);

顺便也贴一下写操作的两个版本的接口:

public abstract Future<Integer> write(ByteBuffer src, long position);

public abstract <A> void write(ByteBuffer src,
                               long position,
                               A attachment,
                               CompletionHandler<Integer,? super A> handler);

我们可以看到,AIO 的读写主要也还是与 Buffer 打交道,这个与 NIO 是一脉相承的。 另外,还提供了用于将内存中的数据刷入到磁盘的方法:

public abstract void force(boolean metaData) throws IOException;

因为我们对文件的写操作,操作系统并不会直接针对文件操作,系统会缓存,然后周期性地刷入到磁盘。如果希望将数据及时写入到磁盘中,以免断电引发部分数据丢失,可以调用此方法。参数如果设置为 true,意味着同时也将文件属性信息更新到磁盘。

还有,还提供了对文件的锁定功能,我们可以锁定文件的部分数据,这样可以进行排他性的操作。

public abstract Future<FileLock> lock(long position, long size, boolean shared);

position 是要锁定内容的开始位置,size 指示了要锁定的区域大小,shared 指示需要的是共享锁还是排他锁

当然,也可以使用回调函数的版本:

public abstract <A> void lock(long position,
                              long size,
                              boolean shared,
                              A attachment,
                              CompletionHandler<FileLock,? super A> handler);

文件锁定功能上还提供了 tryLock 方法,此方法会快速返回结果:

public abstract FileLock tryLock(long position, long size, boolean shared)
    throws IOException;

这个方法很简单,就是尝试去获取锁,如果该区域已被其他线程或其他应用锁住,那么立刻返回 null,否则返回 FileLock 对象。

AsynchronousServerSocketChannel

AsynchronousSocketChannel

AIO socket

AsynchronousChannelGroup

AIO的线程池模型

// 源码