Java提供了哪些IO方式? NIO如何实现多路复用? - 《java核心技术》笔记

简述

  1. 同步阻塞IO库:传统的java.io包下面一些熟知的IO功能,比如File抽象,输入输出流等,交互方式是同步阻塞。java.net下面提供的部分网络API,比如Socket、ServerSocket、HttpURLConnection;
  2. 1.4引入的NIO,提供了Channel、Selector、Buffer等新的抽象,可以用于构建多路复用的、同步非阻塞IO,同时提供了更接近OS底层的高性能数据操作方式;
  3. java7引入的异步非阻塞AIO,基于事件和回调机制。

知识点扩展

相关知识

  1. 同步和异步:同步需要等待当前调用返回,异步是通过事件、回调等机制来实现消息获取;
  2. 阻塞和非阻塞:阻塞时当前线程处于阻塞状态,非阻塞是不管IO操作是否结束都直接返回,相应操作在后台继续处理。

4338e26731db0df390896ab305506d8b

NIO概览

NIO组成部分:

  • Buffer,高效的数据容器
  • Channel,类似于OS的fd,用来支持批量式IO操作的一种抽象;File或Socket通常是高层次抽象,Channel更接近于OS底层的抽象,例如用DMA获得特定场景的性能优化。
  • Selector,NIO实现多路复用的基础,可以检测到注册在Selector上的多个Channel中是否有Channel处于就绪状态,进而实现了单线程对多Channel的高效管理。

NIO解决什么问题

从BIO到NIO到AIO解决一个网络通信的应用。

BIO实现

用accept阻塞等待客户端连接,连接建立后,启动一个单独线程负责回复客户端请求。

public class DemoServer extends Thread {
    private ServerSocket serverSocket;
    public int getPort() {
        return  serverSocket.getLocalPort();
    }
    public void run() {
        try {
            serverSocket = new ServerSocket(0);
            while (true) {
                Socket socket = serverSocket.accept();
                RequestHandler requestHandler = new RequestHandler(socket);
                requestHandler.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                ;
            }
        }
    }
    public static void main(String[] args) throws IOException {
        DemoServer server = new DemoServer();
        server.start();
        try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
            BufferedReader bufferedReader = new BufferedReader(new                   InputStreamReader(client.getInputStream()));
            bufferedReader.lines().forEach(s -> System.out.println(s));
        }
    }
 }
// 简化实现,不做读取,直接发送字符串
class RequestHandler extends Thread {
    private Socket socket;
    RequestHandler(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        try (PrintWriter out = new PrintWriter(socket.getOutputStream());) {
            out.println("Hello world!");
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 }

不足:启动/销毁一个线程由明显开销,每个线程需要占用明显内存以保存单独的线程栈,每个Client单独启动一个线程比较浪费。
解决方法:引入线程池:

serverSocket = new ServerSocket(0);
executor = Executors.newFixedThreadPool(8);
 while (true) {
    Socket socket = serverSocket.accept();
    RequestHandler requestHandler = new RequestHandler(socket);
    executor.execute(requestHandler);
}

da7e1ecfd3c3ee0263b8892342dbc629

不足:当连接数急剧上升时,线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
解决方法:引入多路复用

NIO实现

通过Selector.open()创建一个Selector作为调度员,创建一个ServerSocketChannel并注册到Selector,接下来Selector阻塞在select操作,当有Channel发生接入请求,就会被唤醒。

public class NIOServer extends Thread {
    public void run() {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建 Selector 和 Channel
            serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            serverSocket.configureBlocking(false);//明确配置非阻塞模式
            // 注册到 Selector,并说明关注点
            serverSocket.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();// 阻塞等待就绪的 Channel,这是关键点之一
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                   // 生产系统中一般会额外进行就绪状态检查
                    sayHelloWorld((ServerSocketChannel) key.channel());
                    iter.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void sayHelloWorld(ServerSocketChannel server) throws IOException {
        try (SocketChannel client = server.accept();) {          client.write(Charset.defaultCharset().encode("Hello world!"));
        }
    }
   // 省略了与前面类似的 main
}

前面两个样例都是同步阻塞,所以需要多线程处理多任务。而NIO则是单线程轮询事件,高效的定位就绪的Channel,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁切换线程带来的问题。
ad3b4a49f4c1bff67124563abc50a0a2

AIO实现

使用事件和回调,处理Accept、Read等操作,在accept/read/write等关键节点,通过事件机制调用。

AsynchronousServerSocketChannel serverSock =        AsynchronousServerSocketChannel.open().bind(sockAddr);
serverSock.accept(serverSock, new CompletionHandler<>() { // 为异步操作指定 CompletionHandler 回调函数
    @Override
    public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {
        serverSock.accept(serverSock, this);
        // 另外一个 write(sock,CompletionHandler{})
        sayHelloWorld(sockChannel, Charset.defaultCharset().encode
                ("Hello World!"));
    }
  // 省略其他路径处理方法...
});
comments powered by Disqus