前言
本篇博文是《从0到1学习 Netty》中 NIO 系列的第二篇博文,次要内容是通过 NIO 来了解阻塞模式与非阻塞模式,往期系列文章请拜访博主的 Netty 专栏,博文中的所有代码全副收集在博主的 GitHub 仓库中;
介绍
阻塞模式
在 Java NIO 中,阻塞模式是一种传统的 I/O 解决形式,当咱们试图从通道进行读取或向通道写入数据时,这种模式会使线程阻塞直到操作实现。具体来说,在阻塞模式下,当没有可用的数据时,读操作将被阻塞,直到有数据可读;同样地,当通道已满时,写操作将被阻塞,直到有空间可用于写入。
在网络编程中,应用阻塞模式能够很不便地实现简略、易于了解的代码逻辑。然而,它也有一些毛病。首先,当存在大量连贯时,因为每个连贯都须要一个线程来解决 I/O,因而阻塞模式可能导致系统资源耗尽。其次,在高负载条件下,I/O 操作可能会变得十分迟缓,因为线程必须期待操作实现。因而,对于高并发应用程序,通常应用非阻塞和异步 I/O 模式来进步性能。
非阻塞模式
在 Java NIO 中,非阻塞模式是一种十分重要的概念。在传统的阻塞模式中,当一个线程调用输出或输入操作时,它会始终期待,直到操作实现为止。这意味着,如果有多个客户端申请连贯或发送数据,服务器将不得不创立多个线程来解决每个申请,从而可能导致系统资源耗尽。
非阻塞 I/O(NIO)解决了这个问题,因为它容许应用程序异步地解决多个通道。在非阻塞模式下,当一个线程向通道发出请求并没有立刻失去响应时,该线程能够持续解决其余工作。只有当数据筹备好读取或写入时,线程才会返回通道。这样就能够应用单个线程解决多个连贯和申请。
接下来联合代码进行深刻了解;
Block Server
创立一个
ByteBuffer
用来接收数据;ByteBuffer buffer = ByteBuffer.allocate(16);
创立服务器;
ServerSocketChannel ssc = ServerSocketChannel.open();
绑定监听端口;
ssc.bind(new InetSocketAddress(7999));
创立连贯汇合;
ArrayList<SocketChannel> channels = new ArrayList<>();
accept()
建设与客户端连贯,SocketChannel()
用来与客户端之间通信;SocketChannel sc = ssc.accept();
接管客户端发送的数据;
channel.read(buffer);
残缺代码如下:
import lombok.extern.slf4j.Slf4j;import java.io.IOException;import java.net.InetSocketAddress;import java.net.Socket;import java.nio.ByteBuffer;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.ArrayList;import static com.sidiot.netty.c1.ByteBufferUtil.debugRead;@Slf4jpublic class Server { public static void main(String[] args) throws IOException { // 应用 nio 来了解阻塞模式,单线程 // 1. ByteBuffer ByteBuffer buffer = ByteBuffer.allocate(16); // 2. 创立服务器 ServerSocketChannel ssc = ServerSocketChannel.open(); // 3. 绑定监听端口 ssc.bind(new InetSocketAddress(7999)); // 4. 创立连贯汇合 ArrayList<SocketChannel> channels = new ArrayList<>(); while (true) { // 5. accept 建设与客户端连贯,SocketChannel 用来与客户端之间通信 log.debug("connecting..."); SocketChannel sc = ssc.accept(); log.debug("connected... {}", sc); channels.add(sc); for (SocketChannel channel : channels) { // 6. 接管客户端发送的数据 log.debug("before read... {}", channel); channel.read(buffer); buffer.flip(); debugRead(buffer); buffer.clear(); log.debug("after read... {}", channel); } } }}
Non Block Server
与 Block Server 的代码相相似,然而将 ServerSocketChannel
和 SocketChannel
都设置成了非阻塞模式:
import lombok.extern.slf4j.Slf4j;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.ArrayList;import static com.sidiot.netty.c1.ByteBufferUtil.debugRead;@Slf4jpublic class NonBlockServer { public static void main(String[] args) throws IOException { // 应用 nio 来了解阻塞模式,单线程 // 1. ByteBuffer ByteBuffer buffer = ByteBuffer.allocate(16); // 2. 创立服务器 try (ServerSocketChannel ssc = ServerSocketChannel.open()) { // 设置为非阻塞模式,没有连贯时返回 null,不会阻塞线程 ssc.configureBlocking(false); // 3. 绑定监听端口 ssc.bind(new InetSocketAddress(7999)); // 4. 创立连贯汇合 ArrayList<SocketChannel> channels = new ArrayList<>(); while (true) { // 5. accept 建设与客户端连贯,SocketChannel 用来与客户端之间通信 SocketChannel sc = ssc.accept(); if (sc != null) { log.debug("connected... {}", sc); // 设置为非阻塞模式,若通道中没有数据,会返回 -1,不会阻塞线程 sc.configureBlocking(false); channels.add(sc); } for (SocketChannel channel : channels) { // 6. 接管客户端发送的数据 int read = channel.read(buffer); if (read > 0){ buffer.flip(); debugRead(buffer); buffer.clear(); log.debug("after read... {}", channel); } } } } catch (IOException e) { e.printStackTrace(); } }}
Client
import java.io.IOException;import java.net.InetSocketAddress;import java.nio.channels.SocketChannel;public class Client { public static void main(String[] args) throws IOException { SocketChannel sc = SocketChannel.open(); sc.connect(new InetSocketAddress("localhost", 7999)); System.out.println("waiting..."); }}
剖析
阻塞模式
先启动服务端,会发现因为 accept()
,即没有客户端,导致阻塞发送,控制台输入如下:
10:25:37 [DEBUG] [main] c.s.n.c2.Server - connecting...
接着在客户端的 System.out.println("waiting...");
处设置断点,启用调试模式,控制台输入如下:
10:26:26 [DEBUG] [main] c.s.n.c2.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]10:26:26 [DEBUG] [main] c.s.n.c2.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]
线程第二次进入阻塞状态,这是 channel.read()
导致的,因为没有读取到数据,因而,在客户端的 DEBUG 模式下,右键 sc
属性,抉择 “对表达式求值”:
写入如下代码,点击 “求值”:
控制台输入如下:
当咱们持续 “求值” 时,发现控制台没有输入,这是因为又被 accept()
阻塞了,再新创建一个客户端就能收到音讯了,控制台输入如下:
10:29:53 [DEBUG] [main] c.s.n.c2.Server - connecting...10:37:06 [DEBUG] [main] c.s.n.c2.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:55084]10:37:06 [DEBUG] [main] c.s.n.c2.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]+--------+-------------------- read -----------------------+----------------+position: [0], limit: [3] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 48 69 21 |Hi! |+--------+-------------------------------------------------+----------------+10:37:06 [DEBUG] [main] c.s.n.c2.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:54927]10:37:06 [DEBUG] [main] c.s.n.c2.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:55084]
如果不能启动两个客户端实例的话,须要在配置里进行设置,如下:
非阻塞模式
多启动几个客户端,控制台输入如下:
15:36:52 [DEBUG] [main] c.s.n.c.NonBlockServer - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53122]15:36:56 [DEBUG] [main] c.s.n.c.NonBlockServer - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53129]15:37:08 [DEBUG] [main] c.s.n.c.NonBlockServer - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53136]
按照之前的步骤,通过客户端向服务端发送音讯,控制台输入如下:
+--------+-------------------- read -----------------------+----------------+position: [0], limit: [6] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 73 69 64 69 6f 74 |sidiot |+--------+-------------------------------------------------+----------------+15:37:35 [DEBUG] [main] c.s.n.c.NonBlockServer - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53136]+--------+-------------------- read -----------------------+----------------+position: [0], limit: [12] +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 |Hello World! |+--------+-------------------------------------------------+----------------+15:37:48 [DEBUG] [main] c.s.n.c.NonBlockServer - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:7999 remote=/127.0.0.1:53136]
上述代码存在一个问题,因为设置为了非阻塞,会始终执行 while(true)
中的代码,\( CPU \) 始终处于繁忙状态,会使得性能变低,所以理论状况中不应用这种办法解决申请;
小结
阻塞模式
阻塞模式下,相干办法都会导致线程暂停:
ServerSocketChannel.accept()
会在没有连贯建设时让线程暂停;SocketChannel.read()
会在通道中没有数据可读时让线程暂停;- 阻塞的体现就是让线程暂停,暂停期间不会占用 CPU,但线程相当于闲置;
- 单线程下,阻塞办法之间相互影响,简直不能失常工作,须要多线程反对;
但多线程下,会有一些新的问题:
- 32 位 jvm 一个线程的大小为 320k,64 位 jvm 一个线程的大小为 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能升高;
- 能够采纳线程池技术来缩小线程数和线程上下文切换,但治标不治本,如果有很多连贯建设,长时间 inactive 会阻塞线程池中所有线程,因而不适宜长连贯,只适宜短连贯;
非阻塞模式
- 能够通过
ServerSocketChannel
的configureBlocking(false)
办法将取得连贯设置为非阻塞的。此时若没有连贯,accept
会返回 \( null \); - 能够通过
SocketChannel
的configureBlocking(false)
办法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读,read
会返回 \( -1 \);
后记
以上就是 阻塞模式与非阻塞模式 的所有内容了,心愿本篇博文对大家有所帮忙!
参考:
- Netty API reference;
- 黑马程序员Netty全套教程 ;
上篇精讲:「NIO」(一)意识 ByteBuffer
我是 ,期待你的关注;
创作不易,请多多反对;
系列专栏:摸索 Netty:源码解析与利用案例分享