美文网首页
Java面试题:Tomcat中BIO和NIO线程模型工作原理

Java面试题:Tomcat中BIO和NIO线程模型工作原理

作者: 程序员驴子酱 | 来源:发表于2021-12-03 21:28 被阅读0次

    1.概述

    我们在开发中大多数使用Tomcat作为web服务器,今天我们来讨论一下Tomcat的BIO和NIO数据处理模式,作为丰富我们的Tomcat理论知识大家有必要掌握它们。

    下面我们看一下Tomcat支持的四种线程模式

    线程模式 描述
    BIO 阻塞式IO,即Tomcat使用传统的java.io进行操作。该模式下每个请求都会创建一个线程,对性能开销大,不适合高并发场景。优点是稳定,适合连接数目小且固定架构
    NIO 非阻塞式IO,jdk1.4 之后实现的新IO。该模式基于多路复用选择器监测连接状态在通知线程处理,从而达到非阻塞的目的。比传统BIO能更好的支持并发性能。Tomcat 8.0之后默认采用该模式
    APR 全称是 Apache Portable Runtime/Apache可移植运行库),是Apache HTTP服务器的支持库。可以简单地理解为,Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作。使用需要编译安装APR 库
    AIO 异步非阻塞式IO,jdk1.7后之支持 。与nio不同在于不需要多路复用选择器,而是请求处理

    其实也提供异步非阻塞模式(AIO),但今天我们只研究同步工作原理,后续再给大家讲解异步模式。

    2.BIO模式

    2.1 定义

    BIO:同步阻塞IO(一个连接一个线程),数据的读写必须阻塞在一个线程内等待其完成。例如有一排水壶在烧开水,BIO的工作模式就是叫一个线程停留在一个水壶那儿,直到这个水壶的水烧开才去处理下一个水壶,但实际上线程在等待水壶烧开的时间段什么都没有做。

    BIO是Tomcat8以前的默认IO模式,为了方便大家理解阻塞的特点,我们来写一个IO模型,默认为阻塞模式,其中需要的接口ServerSocketChannel、SocketChannel 、Buffer

    2.2 BIO运行流程

    1. 服务器启动一个serverSocket;

    2. 客户端启动Socket对服务器进行通信,默认情况下服务器需要对每个客户建立一个线程与之通讯;

    3. 客户端发出请求后,先咨询服务器,是否有线程响应,如果没有则会等待,或者被拒绝;

    4. 如果有响应,客户端线程会等待请求结束后,再继续执行。

    2.3 BIO简易模型

    public class Server {
    
       public static void main(String[] args) throws IOException {
    
           ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
           // 监听 8080 端口进来的 TCP 链接
           serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    
           while (true) {
    
               // 这里会阻塞,直到有一个请求的连接进来
               SocketChannel socketChannel = serverSocketChannel.accept();
    
               // 开启一个新的线程来处理这个请求,然后在 while 循环中继续监听 8080 端口
               SocketHandler handler = new SocketHandler(socketChannel);
               new Thread(handler).start();
           }
       }
    }
    

    这里看一下新的线程需要做什么,SocketHandler:

    public class SocketHandler implements Runnable {
    
       private SocketChannel socketChannel;
    
       public SocketHandler(SocketChannel socketChannel) {
           this.socketChannel = socketChannel;
       }
    
       @Override
       public void run() {
    
           ByteBuffer buffer = ByteBuffer.allocate(1024);
           try {
               // 将请求数据读入 Buffer 中
               int num;
               while ((num = socketChannel.read(buffer)) > 0) {
                   // 读取 Buffer 内容之前先 flip 一下
                   buffer.flip();
    
                   // 提取 Buffer 中的数据
                   byte[] bytes = new byte[num];
                   buffer.get(bytes);
    
                   String re = new String(bytes, "UTF-8");
                   System.out.println("收到请求:" + re);
    
                   // 回应客户端
                   ByteBuffer writeBuffer = ByteBuffer.wrap(("我已经收到你的请求,你的请求内容是:" + re).getBytes());
                   socketChannel.write(writeBuffer);
    
                   buffer.clear();
               }
           } catch (IOException e) {
               IOUtils.closeQuietly(socketChannel);
           }
       }
    }
    

    最后,客户端 SocketChannel 的使用,客户端比较简单

    public class SocketChannelTest {
       public static void main(String[] args) throws IOException {
           SocketChannel socketChannel = SocketChannel.open();
           socketChannel.connect(new InetSocketAddress("localhost", 8080));
    
           // 发送请求
           ByteBuffer buffer = ByteBuffer.wrap("1234567890".getBytes());
           socketChannel.write(buffer);
    
           // 读取响应
           ByteBuffer readBuffer = ByteBuffer.allocate(1024);
           int num;
           if ((num = socketChannel.read(readBuffer)) > 0) {
               readBuffer.flip();
    
               byte[] re = new byte[num];
               readBuffer.get(re);
    
               String result = new String(re, "UTF-8");
               System.out.println("返回值: " + result);
           }
       }
    }
    

    以上代码,意味着来一个新的连接,我们就新开一个线程来处理这个连接,之后的操作全部由那个线程来完成。
    那么,这个模式下性能瓶颈在哪里呢?
    1、首先每次来一个连接就开一个线程,对于少量并发请求还可以勉强完成任务,如果大量请求成百上千次就会出现请求阻塞,内存过渡消耗,线程切换的开销非常大。

    2、其次,阻塞操作在这里也是一个问题,accept()默认就是阻塞操作,当请求过来时马上进行新建线程使用SocketChannel,但是这里不代表对方的数据已全部传输过来,所以SocketChannel#read方法将阻塞,等待数据,明显这个等待是不值得的,同理write方法的等待也是不值得的。

    2.4 Tomcat中BIO的工作原理

    由于BIO是Tomcat中的默认运行模式,这里不需要任何设置环境,下面我们看一下它的工作原理:

    1.Tomcat通过Acceptor接受到一个socket链接请求后

    2.Tomcat将该请求封装成一个SocketProcessor连接线程;并放入Executor连接池中

    3.SocketProcessor负责从socket中阻塞读取数据,并且向socket中阻塞写入数据;

    4.最后每一个SocketProcessor对应了一个Http11Processor,并负责解析自己的请求数据。

    2-1634291875649.png

    Tomcat通过Acceptor接收到一个socket链接请求后,会将该请求封装成一个SocketProcessor连接线程,然后将它放入到连接池中。SocketProcessor负责从socket中阻塞读取数据,并且向socket中阻塞写入数据。每个SocketProcessor对应了一个Http11Processor负责解析请求数据。

    说完了阻塞IO模式的缺点,我们再介绍非阻塞IO。

    3.NIO模式

    3.1 定义

    NIO/NIO2 :同步非阻塞(一个线程处理多个请求,多路复用;比如在redis的处理连接的实现),同时支持阻塞和非阻塞,但主要是使用同步非阻塞IO,例如同样有一排水壶在烧水,一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生改变(水烧开),从而进行下一步操作。

    NIO是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。

    java nio 是一个基于缓冲区、并能提供非阻塞I/O踩着的java api,因此nio 也被看成是non-blocking I/O的缩写。 它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio 模式来运行只需要在Tomcat安装目录conf/server.xml 中将对应的protocol的属性值改为 org.apache.coyote.http11.Http11NioProtocol即可。非阻塞IO的核心是使用一个Selector来管理多个通道,可以是SocketChannel,也可以是ServerSocketChannel。

    3.1 Tomcat设置NIO模式:

    1.添加manager/status用户

    <role rolename="manager-gui"/>       
    <user username="tomcat" password="15715746746" roles="manager-gui"/>
    

    2.修改server.xml配置,这里protocol设置 Http11NioProtocol类, 不设置为BIO 。

    <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
                  connectionTimeout="20000"
                  redirectPort="8443" />
    

    3.重启Tocmat

    3-1634292901351.png

    3.2 Tomcat中NIO的工作原理

    NIO作为非阻塞线程IO操作,我们先看一下它的基本思想:

    1.由一个专门的线程处理所有的I/O事件,并负责分发;

    2.事件驱动机制,而不再同步的去监视事件;

    3.线程间通过wait、notify等方式通讯,保证每次上下文切换都是有意义的,减少无谓的线程切换;

    1.png

    通过上图我们解读NIO工作原理

    1.Tomcat利用Acceptor来阻塞获取socket连接,NIO中叫socketChannel;

    2.Acceptor接收到socketChannel后,需要将socketChannel绑定到一个Selector中,并注册读事件;

    3.此时开启一个线程来轮询Selector中是否存在就绪事件,如果存在就将就绪事件查出来,并处理事件,那么负责处理就绪事件的线程对象为“Poller”,每一个Poller中都包含一个Selector,这样每一个Poller线程就负责轮询自己的Selector事件;

    4.然后将处理事件SockectChannel和当前要做的事情(读或写)封装为SocketProcesson对象,并将它放入连接池中,后续步骤则与BIO类似了;

    注意:NIO采用双向通道(channel)进行数据传输,而不是单向的流(stream)。在通道上我们可以注册指定的事件,一共有如下四种事件:

    1、服务器端接收客户端连接事件OP_ACCEPT

    2、客户端连接服务器端事件OP_CONNECT

    3、读事件OP_READ

    4、写事件OP_WRITE

    服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个通道或多个通道上的事情。以服务端为例,如果服务端的selector上注册了读事件时刻客户端给服务端发送了一些数据,BIO这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达则处理这些事件;如果没有感兴趣的事件到达则处理线程会一直阻塞,直到感兴趣的事件到达为止。

    利用java的异步请求I/O处理,可以通过少量的线程处理大量的请求

    注意:Tomcat 8 以上版本在linux系统中,默认使用的就是NIO模块,不需要额外的修改,Tomcat7 必须修改Connector配置来启动

    相关文章

      网友评论

          本文标题:Java面试题:Tomcat中BIO和NIO线程模型工作原理

          本文链接:https://www.haomeiwen.com/subject/noxuxrtx.html