开发必学,io&nio
作为一个程序开发人员,不可避免的要与io打交道,通常我们也都会在简历栏目上写上熟悉or了解io,那么你是否真的了解io与nio的区别呢?【划重点:面试官常问点】
首先,在详细描述io与nio的区别之前我们要先意识到
所有的系统I/O都分为两个阶段:等待就绪和操作。
并且等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",也意味着会消耗cpu资源。
为了让大家有个更加直观的感受,这里先抛出基于io和基于nio实现的例子。
io实现
package test;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class IOServer {
public static void main(String[] args) throws Exception {
init();
}
private static void init() throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
new Thread(() -> {
while (true) {
// 阻塞点,获取新的连接
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
// 创建线程点,给每一个新的连接都创建一个线程
Socket finalSocket = socket;
new Thread(() -> {
int len;
byte[] data = new byte[1024];
InputStream inputStream = null;
try {
inputStream = finalSocket.getInputStream();
// 面向流,按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}).start();
}
}
可以从上面的例子看出Server 端首先创建了一个serverSocket
来监听 8000 端口,然后创建一个线程,线程里面死循环不断调用阻塞方法 serversocket.accept();
获取新的客户端连接,当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据然后读取数据是以字节流的方式。
这里的弊端是极其明显的,如
-
线程资源受限:上面每个客户端都会构建一个线程去做读取操作,而线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起。
-
线程切换效率低下:线程爆炸之后带来的副作用便是操作系统频繁进行线程切换,应用性能急剧下降。
nio实现
package test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
init();
}
private static void init() throws IOException {
// 构建Selector
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
// 开启一个线程处理客户端的连接
new Thread(() -> {
try {
// 此处对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 使用Selector来监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// 同io不同的地方,每来一个新连接,没有再创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
// 开启一个线程处理多个客户端线程的数据读取
new Thread(() -> {
try {
while (true) {
// 轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 同io不同的地方,面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
可以通过上面代码看出,
NIO 实现给出了两个线程,每个线程都绑定一个轮询器 selector ,如serverSelector
负责轮询是否有新的连接,而clientSelector
负责轮询判断客户端的连接是否有数据可读,和io实现的区别点有:
-
在服务端监测到新的连接之后,不会很sb的创建一个新的线程,而是直接将新连接绑定到
clientSelector
上,这样就不会消耗那么多线程资源了。 -
clientSelector
则会进行判断如果在某一时刻有多条连接有数据可读,那么通过clientSelector.select(1)
方法可以轮询出来,进而处理客户端数据。 -
数据的读写面向 Buffer。
因此我们可以得出结论:io是面向流的阻塞io,而nio是面向缓冲区的非阻塞io
所谓的阻塞io和非阻塞io:
- 阻塞io意味着当线程调用read()或write()时,该线程将会被阻塞,直到有一些数据要读取,或者数据被完全写入。而在此期间,被阻塞的线程将无法执行任何其他操作。
- 非阻塞io指的是允许线程请求从通道读取数据,如果通道没有数据可以读取的时候,线程可以继续使用其他内容,而不是在数据可供读取之前保持阻塞状态。
所谓的面向流和面向缓冲区:
- 面向流的Java IO意味着可以从流中一次读取一个或多个字节,它们不会缓存在任何地方。此外,我们无法在流中的数据中前后移动。如果需要在从流中读取的数据中进行前后移动,则需要先将其缓存在缓冲区中。
- 面向缓冲区的Java-NIO则是将数据读入缓冲区。我们可以根据需要在缓冲区中进行前后移动。这可以让我们在处理过程中更具灵活性。但是,我们还需要检查缓冲区是否包含完整处理所需的所有数据。当然了,还需要确保在将更多数据读入缓冲区时,不会出现bug,比如覆盖了尚未处理的缓冲区中的数据。
系列博客可以关注公众号:
公众号.jpg