初学Java NIO

Java中目前有两套I/O库,分别是BIO(Blocking I/O,位于java.io包)与NIO(Non-blocking I/O,位于java.nio包)。
在Java4时,Java引入了新的I/O库,即Java NIO。实际上,正如《Java编程思想》中所述,旧的IO包已经使用NIO重新实现过了(详见此文,诸如FileInputStream等类的read方法已用NIO重写),所以正如许多人所言,“即使我们不显式的使用NIO编程,也能从中受益”。

但是NIO作为一个很有意思的思想,值得去好好研究一番。

本文参考以下内容:

  1. 美团技术团队: Java NIO浅析
  2. Jakob Jenkov: Java NIO Tutorial

注:上文提到Java有两套I/O库,说法并不准确,实际上还有AIO。

回看BIO

在BIO中,最主要的概念是Stream(流),Stream是单向的,即分为InputStream和OutStream。

以网络IO场景为例,下面是一个典型的服务端BIO模型:

public class BIOTest {

    public static final int PORT = 2333;

    public static void main(String[] args) throws IOException {
        ExecutorService executor = Executors.newFixedThreadPool(100);
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("localhost", PORT));
        while (!Thread.currentThread().isInterrupted()) {
            Socket socket = serverSocket.accept();
            executor.submit(new ConnectIOnHandler(socket));
        }
    }

    static class ConnectIOnHandler extends Thread {
        private final Socket socket;

        public ConnectIOnHandler(Socket socket) {
            this.socket = socket;
        }

        public void run() {
            while (!Thread.currentThread().isInterrupted() && !socket.isClosed()) {
                try {
                    InputStream is = socket.getInputStream();
                    OutputStream os = socket.getOutputStream();
                    //todo

                    socket.close();  //close socket

                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

在这个模型中,阻塞了主线程且建立了一个线程池,每个连接都对应一个线程。
之所以要为每个连接开一个线程,是因为BIO模型下的读写操作都是同步阻塞的,单线程显然是不可以的。这里使用了一个线程池来管理连接请求线程,使用线程池可以重复利用已经创建的线程,能够提高资源利用率。

但是这里最核心的问题是,每个连接都对应了一个线程,开一个线程的代价可不小,他们在系统中本质都是较为重量级的系统调用,并且线程的切换成本也很高,且需要消耗一定量的系统资源,如果线程的数量较多,会给系统带来不可忽视的负担。
针对用户量较大的应用而言,容易出现锯齿状的用户负载,例如网络不畅可能导致大量请求同时返回,这可能会同时激活大量阻塞的线程,导致系统负载压力过大。对于暂时限制的情况而言,在开始限制到请求超时的时间内,线程处于空闲状态,对于资源而言是一种浪费,线程在空闲态下无法被有效利用,表现在用户量较大的场景下浪费尤其明显,这种现象在用户网络不畅的情况下表现更频繁。

Java NIO是一套非阻塞IO API。不同于BIO面向流的IO形式,NIO面向缓冲区。面向流的BIO以一次处理一个字节的形式处理数据,NIO以一次处理一个块(整个缓冲区)的形式处理数据。

NIO的概念

NIO是同步非阻塞的,它的三个核心概念为:Channel(通道),Buffer(缓冲区),Selector(选择器)。

在NIO中,Channel关注的工作是数据怎么读与怎么写,也就是他只负责传输数据,就只是经个手,不直接去碰被操作的数据。Channel与Stream不同,它同时可以具备读与写数据的能力,这使得读写操作是双向的了。我们不能直接通过Channel读写数据,而必须要通过一个“媒介”操作Channel,这个“媒介”就是Buffer。
我们的程序可以将数据读或写到一个Buffer里,并可以将Buffer里的内容通过Channel写出,或者从Channel里读一份数据到Buffer里。
如果同时有多个Channel,Selector可以检测多个Channel,他可以帮助选择出已经就绪的Channel,当某个Channel有事件就绪时,线程可以通过Selector处理,实现一个线程管理多个Channel的目的。Selector在网络IO场景下是一个很有用的概念。

可以这样理解,BIO里我们为每个请求开了一个线程,相当于舔狗,一直舔着socket巴结人家问能不能读数据了。在NIO里,相当于批阅奏折的皇上,socket相当于臣子,只有在有人送奏折的时候才会处理。

NIO使得socket在等待阶段请求是非阻塞的,真正的I/O操作是同步阻塞的,虽然消耗CPU但是性能较高。

上一篇
下一篇