点击量:639
一、基本概念
Blocking IO VS NonBlocking IO
阻塞IO:线程会一直等待/阻塞直到IO操作完成,比如准备读取10个字节的数据,但是现在只读了8个,那么当前线程会一直等下去,直到读取到剩下的2个字节的数据。这是普通IO的线程模型:

非阻塞IO:在进行IO操作时线程并不会阻塞(等待),一次只读取能读取的数据,读完立刻返回,然后线程会去继续处理其他事情。等下次可读的时候被唤醒,再来读取数据。这是NIO的线程模型:

因此在阻塞IO模式下,如果你要处理N个请求的话,就需要开启N个线程分别处理这些请求。因此最大的连接数取决于服务器能开出的最大线程数,虽然后期采用线程池的方式做了些优化,但总体而言性能并没有很大提升。而在非阻塞IO下只要一个线程就够了,不断的在各个连接之间切换,在某个连接有可用事件的时候通知主线程。
咋一看是不是很容易理解?但是对于NIO的工作模式我一直有个疑问:
主线程立即返回了,那读写数据的时候总得消耗线程吧?
其实真正的IO操作是内核线程,但上层也需要用户线程去调用。而且NIO所说的用户线程立刻返回并不是说不消耗线程,而是对线程的占用程度不一样,阻塞IO会一直占用线程直到IO操作完成,而NIO则只会占用注册事件可用的那段时间

二、代码层面理解NIO
这是一个标准的基于阻塞IO的网络服务器的写法:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class PlainOioServer { public void serve(int port) throws IOException { final ServerSocket socket = new ServerSocket(port); try { while (true) { final Socket clientSocket = socket.accept(); System.out.println("Accepted connection from " + clientSocket); new Thread(new Runnable() { @Override public void run() { try { //#fragment1 starts InputStream in = clientSocket.getInputStream(); InputStreamReader reader = new InputStreamReader(in); BufferedReader buffer = new BufferedReader(reader); String line = reader.readLine(); System.out.println("#readLine:"+line); clientSocket.close(); //#fragment1 ends } catch (IOException e) { e.printStackTrace(); try { clientSocket.close(); } catch (IOException ex) { // ignore on close } } } }).start(); } } catch (IOException e) { e.printStackTrace(); } } } |
这是一个基于NIO的网络服务器的写法:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public class PlainNioServer { public void serve(int port) throws IOException { System.out.println("Listening for connections on port " + port); ServerSocketChannel serverChannel; Selector selector; serverChannel = ServerSocketChannel.open(); ServerSocket ss = serverChannel.socket(); InetSocketAddress address = new InetSocketAddress(port); ss.bind(address); serverChannel.configureBlocking(false); selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { try { selector.select(); } catch (IOException ex) { ex.printStackTrace(); // handle in a proper way break; } Set<SelectedKey> readyKeys = selector.selectedKeys(); Iterator<SelectedKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel client = server.accept(); System.out.println("Accepted connection from " + client); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ, msg.duplicate()); } if (key.isReadable()) { //#fragment2 starts SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(48); while (true) { buffer.clear(); int n = sc.read(buffer);//读取数据 if (n <= 0) { //全部读完退出 break; } buffer.flip(); } it.remove(); //#fragment2 ends } } catch (IOException ex) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { } } } } } } |
几乎所有的介绍NIO的文章都会贴出这两段代码来解释IO和NIO的区别,告诉你下面这段代码采用了NIO,然后就开始介绍Java NIO各种基本组件,比如channel,buffer,selector等,但是几乎所有博主都没有认真分析过这段代码是怎么体现NIO的。
代码片段fragment1和fragment2都在读数据,NIO只不过变成异步的方式去触发读写,但是读写这部分该消耗的还是要消耗啊,因为总的数据量是一样的啊。So, what’s the difference?
根本的原因是:数据的传输是一个持续的过程,但数据读写的并不一定是连续的。也就是说服务器和客户端的连接一直在,但他们之间的数据并不是一直有,而是断断续续的。普通IO的方法在没有数据的时候也会一直等,而NIO不会,只会在selector通知可读的时候才会来把buffer读满,根本木有等待的过程。理解这段代码要从最大的wihle循环入手,动态的模拟代码的执行。NIO通过不断的循环(事件通知)来读取数据,每次读取一小段,最后累加起来把所有的数据读完,而不是像阻塞IO那样一次性读完。
三、NIO与异步编程模型
此外,我们发现这段代码只有一个线程,当他去处理某个请求(读写数据)时是会阻塞的,其他请求就进不来了。这么看接收请求的能力并不比传统的IO强多少啊。注意,这段代码想说明的是NIO,与NIO相关的是IO操作的线程,与接收请求线程,处理请求线程这两个没有关系。不要以为有一个boss线程分发到请求到worker线程就是NIO了(第一段代码就是这种异步处理模式),NIO与这个线程模型没有关系,NIO的核心是selector中的事件通知。在实际使用的时候,接受请求和处理请求的线程是分开的,前者一般是一个线程,而后者会采用线程池的方式增加效率(netty就是这种方式)。而连接池中的线程会在各个channel中不停地切换(一个线程不会被一个channel占满),相比于传统的IO线程模型,在这种模式性能自然是提高了不少。
这篇日志写完理解又加深了一点(上面写的好啰嗦),其实概括下来就几点:
1.阻塞并不是IO读写,而是在IO等待(理解概念要细致具体!)
2.线程在没有数据的情况下(不可读也不可写)不会傻等,这就是NIO的核心概念。