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作为一个很有意思的思想,值得去好好研究一番。
本文参考以下内容:
注:上文提到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但是性能较高。