NIO下理解阻塞模式和非阻塞模式


一、阻塞和非阻塞

何为阻塞?

从该网络通讯过程来理解一下何为阻塞 :

在以上过程中若连接还没到来,那么 accept 会阻塞 , 程序运行到这里不得不挂起, CPU 转而执行其他线程。

在以上过程中若数据还没准备好, read 会一样也会阻塞。

阻塞式网络 IO 的特点:多线程处理多个连接。每个线程拥有自己的栈空间并且占用一些 CPU 时间。每个线程遇到外部为准备好的时候,都会阻塞掉。阻塞的结果就是会带来大量的进程上下文切换。且大部分进程上下文切换可能是无意义的。比如假设一个线程监听一个端口,一天只会有几次请求进来,但是该 cpu 不得不为该线程不断做上下文切换尝试,大部分的切换以阻塞告终。

何为非阻塞?

下面有个隐喻:

一辆从 A 开往 B 的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?

  1. 司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。 ( 类似阻塞式 )

  2. 每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 ( 类似非阻塞 )

很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是 CPU 。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。

二、阻塞

1、阻塞

  • 阻塞模式下,相关方法都会导致线程暂停
    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
    • SocketChannel.read 会在没有数据可读时让线程暂停
    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,体现在以下方面
    • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

2、代码演示

下面是案例:

首先我们写一个Socket客户端,代码如下,具体解析都加有相应的注释:

import com.example.nettydemo.c1.test.ByteBufferUtil;
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 java.util.List;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        // 使用nio来理解阻塞模式。这里使用的是单线程连接
        // 全局ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 1、创建服务器
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        // 2、绑定监听端口
        socketChannel.bind(new InetSocketAddress(8080));

        // 3、连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while (true) {
            // 4、accept与客户端建立连接,SocketChannel用来与客户端之间通信
            log.debug("connecting..."); //accept() 默认是阻塞方法,线程停止运行等待
            SocketChannel sc = socketChannel.accept();
            log.debug("connected... {}", sc);
            channels.add(sc);
            for (SocketChannel channel : channels) {
                // 5、接收客户端发送的数据
                log.debug("before read... {}", channel);
                channel.read(buffer); //read()也是一个阻塞方法,线程停止运行等待
                buffer.flip(); //切换为读模式
                ByteBufferUtil.debugRead(buffer);  // 调试打印数据
                buffer.clear(); //切换为写模式,并重置position
                log.debug("after read... {}", channel);
            }
        }

    }
}

服务端用来接收数据,我们还需要一个客户端进行发送数据,客户端比较简单,如下示例:

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连接
        SocketChannel socketChannel = SocketChannel.open();
        // 连接本地,监听相应端口号
        socketChannel.connect(new InetSocketAddress("localhost",8080));
        // debug模式下
        // 执行表达式  socketChannel.write(Charset.defaultCharset().encode("hello"))
        // 可以看到运行的结果
        System.out.println("waiting ....");
    }
}

下面我们进行模拟客户端与服务端之间发送和接收数据(单线程),我们以正常模式启动Server服务端,控制台会打印以下信息:

16:22:44 [DEBUG] [main] c.e.n.c.t.Server - connecting...

这是因为accept()方法默认是阻塞方法,当线程执行到accept()方法的时候,线程会进入阻塞状态,等待客户端进行发送数据,此时线程不会进行其它操作。
接着我们以debug模式启动Client客户端,在输出语句的地方打上一个断点,保证不影响Socket的连接。
启动后,发现控制台又打印了两条信息,如下:

16:22:44 [DEBUG] [main] c.e.n.c.t.Server - connecting...
16:25:24 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
16:25:24 [DEBUG] [main] c.e.n.c.t.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]

当accept()方法等待到客户端的连接之后,会继续向下执行,建立连接,准备读取数据,这时我们知道ByteBuffer的read()方法也是一个阻塞方法,此时该线程又会被阻塞,等待数据的读取。

我们可以使用debug的Evaluate Expression功能来执行模拟发送数据,操作如下:
在这里插入图片描述
我们执行这样一条表达式 socketChannel.write(Charset.defaultCharset().encode("hello"))
在这里插入图片描述
这就模拟发送了一条数据 “hello”,我们查看控制台打印信息,如下:

16:22:44 [DEBUG] [main] c.e.n.c.t.Server - connecting...
16:25:24 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
16:25:24 [DEBUG] [main] c.e.n.c.t.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [5]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
16:29:44 [DEBUG] [main] c.e.n.c.t.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
16:29:44 [DEBUG] [main] c.e.n.c.t.Server - connecting...

数据发送成功,而且服务端也接收到了数据,并且读取数据完毕之后,又进入了连接等待状态。

那么我们尝试再次发送一条数据,执行如下表达式 socketChannel.write(Charset.defaultCharset().encode("hi"))

再次查看控制台,发现控制台什么也没有打印,说明服务端Server并未接收到客户端的数据,这是为什么呢 ?

我们看原来的服务端代码,发现,SocketChannel又执行了accept()方法,等待新的客户端建立连接,而我们并没有启动新的客户端,所以该线程被阻塞了,不会执行其它任何的操作。
在这里插入图片描述
发现了问题所在,那么我们再建立一个客户端不就好了么?
我们可以通过IDEA配置,进行多个程序的运行,如图:
(1)打开客户端编辑配置
在这里插入图片描述
(2)选择以下选项
我这里已经勾选过了。
在这里插入图片描述

再次以debug模式运行Client客户端,运行成功后,发现服务端Server控制台打印了相关信息,如下:

16:22:44 [DEBUG] [main] c.e.n.c.t.Server - connecting...
16:25:24 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
16:25:24 [DEBUG] [main] c.e.n.c.t.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [5]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
16:29:44 [DEBUG] [main] c.e.n.c.t.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
16:29:44 [DEBUG] [main] c.e.n.c.t.Server - connecting...
16:37:06 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37084]
16:37:06 [DEBUG] [main] c.e.n.c.t.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [2]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69                                           |hi              |
+--------+-------------------------------------------------+----------------+
16:37:06 [DEBUG] [main] c.e.n.c.t.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:36948]
16:37:06 [DEBUG] [main] c.e.n.c.t.Server - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37084]

我们可以与之前打印的信息相比较,我们这次没有使用debug模拟发送数据,但是之前发送的数据(并未被接收的)现在被接收并且打印了。

3、结论

所以我们得出结论:
(1)以上过程是理解阻塞的过程。
(2)单线程下,阻塞方法之间相互相应,几乎不能正常工作。
(3)多线程方式下产生新的问题,这里不再演示

二、非阻塞

1、非阻塞

  • 非阻塞模式下,相关方法都会不会让线程暂停
    • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
    • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
    • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
  • 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu
  • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

二、代码演示

下面我们继续演示非阻塞模式的过程。
与阻塞模式相同,我们需要一个客户端和服务端,两者的代码差不多,这里就不再进行重写,给出服务端Server的代码,客户端的代码不变。

import com.example.nettydemo.c1.test.ByteBufferUtil;
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 java.util.List;

@Slf4j
public class Server {
    public static void main(String[] args) throws IOException {
        // 使用nio来理解阻塞模式。这里使用的是单线程连接
        // 全局ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 1、创建服务器
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.configureBlocking(false); // 切换成非阻塞模式
        // 2、绑定监听端口
        socketChannel.bind(new InetSocketAddress(8080));

        // 3、连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while (true) {
            // 4、accept与客户端建立连接,SocketChannel用来与客户端之间通信
            //accept() 默认是阻塞方法,线程停止运行等待
            //由于上面的设置,该方法就变成了非阻塞方法,线程会继续运行
            //如果没有连接建立,那么sc返回的是null,建立连接则不是null
            SocketChannel sc = socketChannel.accept();
            if (sc != null) {
                log.debug("connected... {}", sc);
                //将SocketChannel设置成非阻塞方法
                sc.configureBlocking(false);
                channels.add(sc);
            }
            for (SocketChannel channel : channels) {
                // 5、接收客户端发送的数据
                //read()也是一个阻塞方法,线程停止运行等待
                //SocketChannel设置成非阻塞方法,那么read()也就是非阻塞方法
                //如果没有读取到数据,read返回0
                int read = channel.read(buffer);
                if (read > 0){
                    buffer.flip(); //切换为读模式
                    ByteBufferUtil.debugRead(buffer);  // 调试打印数据
                    buffer.clear(); //切换为写模式,并重置position
                    log.debug("after read... {}", channel);
                }
            }
        }

    }
}

其实非阻塞模式只是将socket的模式设置成为了非阻塞模式,我们先以正常模式运行服务端Server,因为我们在客户端的代码中进行了非null判断,所以我们将一些debug的日志删掉了,防止刷屏影响我们查看控制台数据。

然后我们以debug模式启动三个客户端Client,启动后发现控制台输出信息:

16:49:10 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37268]
16:49:32 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37279]
16:49:55 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37288]

说明服务端Server都能接收到客户端Client的连接。

然后我们打开端口号为37288的客户端,使用debug工具模拟发送消息,控制台打印如下:
socketChannel.write(Charset.defaultCharset().encode("hello"))

16:49:10 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37268]
16:49:32 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37279]
16:49:55 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37288]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [5]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
16:50:47 [DEBUG] [main] c.e.n.c.t.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37288]

我们再次发送一条数据,socketChannel.write(Charset.defaultCharset().encode("hi")),查看控制台,发现仍能打印出数据,说明服务端能够一直接收客户端的数据。

16:49:10 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37268]
16:49:32 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37279]
16:49:55 [DEBUG] [main] c.e.n.c.t.Server - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37288]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [5]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
16:50:47 [DEBUG] [main] c.e.n.c.t.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37288]
+--------+-------------------- read -----------------------+----------------+
position: [0], limit: [2]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69                                           |hi              |
+--------+-------------------------------------------------+----------------+
16:51:06 [DEBUG] [main] c.e.n.c.t.Server - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:37288]

其他的客户端发送数据也是可以被服务端接收的到的。

3、结论

(1)非阻塞模式下,相关方法都会不会让线程暂停
(2)但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值