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

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用户

       

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


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线程模型工作原理)