关于java:异步Servlet学习笔记一

2次阅读

共计 11276 个字符,预计需要花费 29 分钟才能阅读完成。

两周没更新了,感觉不写点什么,有点不难受的感觉。

前言

回顾一下学 Java 的历程,过后是看 JavaSE(根本语法、线程、泛型),而后是 JavaEE,JavaEE 也根本就是围绕着 Servlet 的应用、JSP、JDBC 来学习,过后看的是 B 站 up 主颜群的教学视频:

  • JavaWeb 视频教程(JSP/Servlet/ 上传 / 下载 / 分页 /MVC/ 三层架构 /Ajax)https://www.bilibili.com/video/BV18s411u7EH?p=6&vd_source=aae…

当初一看这个播放量破百万了,当初我看的时候应该播放量很少,当初这么多倒是有点昨舌。学完了这个之后,开始学习框架:Spring、SpringMVC、MyBatis、SpringBoot。尽管 Spring MVC 实质上也是基于 Servlet 做封装,但前面根本就转型成 Spring 工程师了,最近碰到一些问题,又看了一篇文章,感觉一些问题之前本人还是没思考到,颇有种离了 Spring 家族,不会写后端一样。原本明天的行文最后是什么是异步 Servlet,异步 Servlet 该如何应用。然而想想没有切入实质,所以将其换成了对话体。

注释

咱们接着有请之前的实习生小陈,每当咱们须要用到对话体、故事体这样的行文。实习生小陈就会出场。明天的小陈呢感觉行情有些不好,然而还是感觉想进来看看,毕竟金三银四,于是下午就向领导销假去面试了。进到面试的中央,一番自我介绍,面试官首先问了这样一个问题:

一个申请是怎么被 Tomcat 所解决的呢?

小陈答复到:

我目前用的都是 Spring Boot 工程,我看都是启动都是在 main 函数外面启动整个我的项目的,而 main 函数又被 main 线程执行,所以我想应该是申请过去之后,被 main 线程所解决,给出响应的。

面试官:

╮(╯▽╰)╭,main 函数确实是被 main 线程执行,但都是被 main 线程解决的?这不合理吧,假如某个申请占用了 main 线程三秒,那这三秒内,零碎都无奈再回应申请了。你要不再想想?

小陈挠了挠头,接着答到:

的确是,浏览器和 Tomcat 通信用的是 HTTP 协定,我也学过网络编程,所以我感觉应该是一个线程一个申请吧。像上面这样:

public class ServerSocketDemo {private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(4);

    public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket(8080);
        while (true){
            // 一个 socket 对象代表一个连贯
            // 期待 TCP 连贯申请的建设, 在 TCP 连贯申请建设实现之前, 会陷入阻塞
            Socket socket = serverSocket.accept();
            System.out.println("以后连贯建设:"+ socket.getInetAddress().getHostName()+socket);
            EXECUTOR_SERVICE.submit(()->{
                try {
                    // 从输出流中读取客户端发送的内容
                    InputStream inputStream = socket.getInputStream();
                    // 从输入流里向客户端写入数据
                    OutputStream outPutStream = socket.getOutputStream();} catch (IOException e) {e.printStackTrace();
                }
            });
        }
    }
}

serverSocket 的 accept 在连贯建设起来会陷入阻塞。

面试官点了拍板, 接着问到:

你这个 main 线程负责检测连贯是否建设,而后建设之后将后续的业务解决放入线程池,这个是 NIO 吧。

小陈笑了笑说道:

尽管我对 NIO 理解不多,但这应该也不是 NIO,因为前面的线程在期待数据可读可写的过程中会陷入阻塞。在操作系统中,线程是一个相当低廉的资源,咱们个别应用线程池,能够让线程的创立和回收老本绝对较低,在流动连接数不是特地高的状况下(单机小于 1000),这种,模型是比拟不错的,能够让每一个连贯专一于本人的 I / O 并且编程模型简略。但要命的就是在连贯上来之后,这种模型呈现了问题。咱们来剖析一下咱们下面的 BIO 模型存在的问题,主线程在承受连贯之后返回一个 Socket 对象,将 Socket 对象提交给线程池解决。由这个线程池的线程来执行读写操作,那事实上这个线程承当的工作有判断数据可读、判断数据可写,对可读数据进行业务操作之后,将须要写入的数据进行写入。那陷入阻塞的就是在期待数据可写、期待数据可读的过程,在 NIO 模型下对本来一个线程的工作进行了拆分,将判断可读可写工作进行了拆散或者对原先的模型进行了革新,原先的业务解决就只做业务解决,将判断可读还是可写、以及写入这个工作专门进行拆散。

咱们将判断可读、可写、有新连贯建设的线程权且就称之为 I / O 主线程吧,这个主线程在一直轮询这三个事件是否产生,如果产生了就将其就给对应的处理器。这也就是最简略的 Reactor 模式: 注册所有感兴趣的事件处理器,单线程轮询抉择就绪事件,执行事件处理器。

当初咱们就能够大抵总结进去 NIO 是怎么解决掉线程的瓶颈并解决海量连贯的: 由原来的阻塞读写变成了单线程轮询事件,找到能够进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有满足的事件就必须要要阻塞),残余的 I / O 操作都是纯 CPU 操作,没有必要开启多线程。

面试官点了拍板,说道:

还能够嘛,小伙子,刚刚问怎么卡 (qia) 壳了?

小陈不好意思的挠挠头, 笑道:

其实之前看过这部分内容,只不过可能常识不必就想不起来,您提醒了一下,我才想起来。

面试官笑了一下,接着问:

那当初的服务器,个别都是多核解决,如果可能利用多外围进行 I /O,无疑对效率会有更大的进步。是否对下面的模型进行继续优化呢?

小陈想了想答道:

认真分一下咱们须要的线程,其实次要包含以下几种:

  1. 事件散发器,单线程抉择就绪的事件。
  2. I/ O 处理器,包含 connect、read、writer 等,这种纯 CPU 操作,个别开启 CPU 外围个线程就能够了
  3. 业务线程,在解决完 I / O 后,业务个别还会有本人的业务逻辑,有的还会有其余的阻塞 I /O,如 DB 操作,RPC 等。只有有阻塞,就须要独自的线程。

面试官点了拍板,接着问道:

不错,不错。那 Java 的 NIO 晓得嘛。

小陈点了拍板说道:

晓得,Java 引入了 Selector、Channel、Buffer,来实现咱们新建设的模型,Selector 字面意思是选择器,负责感应事件,也就是咱们下面提到的事件散发器。Channel 是一种对 I / O 操作的形象,能够用于读取和写入数据。Buffer 则是一种用于存储数据的缓冲区,提供对立存取的操作。

面试官又问道:

有理解过 Java 的 Selector 在 Linux 零碎下的限度嘛?

小陈答道:

Java 的 Selector 对于 Linux 零碎来说,有一个致命的限度: 同一个 channel 的 select 不能被并发的调用。因而,如果有多个 I / O 线程,必须保障: 一个 socket 只能属于一个 IO 线程,而一个 IO 线程能够治理多个 socket。

面试官点了拍板:

不错,不错。Tomcat 有罕用的默认配置参数有: acceptorThreadCount、maxConnections、maxThreads。解释一下这几个参数的意义,并且给出一个申请在达到 Tomcat 之后是怎么被解决的,要求联合 Servlet 来进行阐明。

小陈深思了一下道:

acceptorThreadCount 用来管制接管连贯的线程数,如果服务器是多外围,能够调大一点。然而 Tomcat 的官网文档倡议不要超过 2 个。管制接管连贯这部分的代码在 Acceptor 这个类里,你能够看到这个类是 Runnable 的实现类。在 Tomcat 的 8.0 版本,你还能查到这个参数的阐明,然而在 8.5 这个版本就查不到,我没找到对应的阐明,然而在 Tomcat 9.0 源码的 AbstractProtocol 类中的 setAcceptorThreadCount 办法能够看到,这个参数被废除,下面还有阐明,说这个参数将在 Tomcat 的 10.0 被移除。maxConnections 用于管制 Tomcat 可能接受的 TCP 连接数,当达到最大连接数时,操作系统会将申请的连贯放入到队列外面,这个队列的数目由 acceptCount 这个参数管制,默认值为 100,如果超过了操作系统能接受的连贯数目,这个参数也会不起作用,TCP 连贯会被操作系统回绝。maxConnections 在 NIO 和 NIO2 下, 默认值是 10000,在 APR/native 模式下,默认值是 8192.

maxThreads 管制最大线程数,一个 HTTP 申请默认会被一个线程解决,也就是一个 Servlet 一个线程,能够这么了解 maxThreads 的数目决定了 Tomcat 可能同时解决的 HTTP 申请数。默认为 200。

面试官仿佛很称心,点了拍板,接着道:

小伙子,看的还挺多,NIO 下面你曾经讲了, NIO2 和 APR 是什么,你有理解过嘛?

小陈考虑了一下答复到:

我先来介绍 APR 吧,APR 是 Apache Portable Runtime 的缩写,是一个为 Tomcat 提供扩大能力的库,之所以带上 native 的起因是 APR 不应用 Java 编写的连接器,而是抉择间接调用操作系统,防止了 JVM 级别的开销,实践上性能会更好。NIO2 加强了 NIO,咱们先在只探讨网络方面的加强,NIO 下面咱们是启用了轮询来判断对应的事件是否能够进行,NIO2 则引入了异步 IO,咱们不必再轮询,只用接管操作系统给咱们的告诉。

面试官:

当初咱们将下面的问题连贯在一起,向 Tomcat 应用服务器收回 HTTP 申请,在 NIO 模式下,这个申请是如何被 Tomcat 所解决的。

小陈道:

申请会首先达到操作系统,建设 TCP 连贯,这个过程由操作系统实现,咱们临时疏忽,当初这个连贯申请实现达到了 Acceptor(连接器),连接器在 NIO 模式下会借助 NIO 中的 channel,将其设置为非阻塞模式,而后将 NioChannel 注册到轮询线程上,轮询工作由 Poller 这个类来实现,而后由 Poller 将就绪的事件生成 SocketProcessor, 交给 Excutor 去执行,Excutor 这是一个线程池,线程池的大小就是在 Connector 节点配置的 maxThreads 的值,这个线程池解决的工作为:

  1. 从 socket 中读取 http request
  2. 解析生成 HttpServletRequest 对象
  3. 分派到相应的 servlet 并实现逻辑
  4. 将 response 通过 socket 发回 client。

面试官:

这个线程池,你有理解过嘛?

小陈道:

这个线程池不是 JDK 的线程池,继承了 JDK 的 ThreadPoolExecutor, 本身做了一些扩写,我看网上的一些博客是说的是这个 ThreadPoolExecutor 跟 JDK 的 ThreadPoolExecutor 行为不太统一,JDK 外面的 ThreadPoolExecutor 在接管到工作的时候是,看以后线程池沉闷的线程数目是否小于外围线程数,如果小于就创立一个线程来执行以后提交的工作,如果以后沉闷的线程数目等于外围线程数,那么就将这个工作放到阻塞队列中,如果阻塞队列满了,判断以后沉闷的线程数目是否达到最大线程数目,如果没达到,就创立新线程去执行提交的工作。当工作处理完毕,线程池中沉闷的线程数超过外围线程池数,超出的在存活 keepAliveTime 和 unit 的工夫,就会被回收。简略的说,就是 JDK 的线程池是先外围线程,再队列,最初是最大线程数。我看到的一些博客说 Tomcat 是先外围线程,再最大线程数,最初是队列。然而我看了 Tomcat 的源码,在 StandardThreadExecutor 执行工作的时候还是调用父类的办法,这让我很不解,先外围线程,再最大线程数,最初是队列,这个论断是怎么得进去的。

面试官点了拍板:

还不错,蛮有实证精力的,看了博客还会本人去验证。我还是蛮观赏你的,你过去一下,咱们看着源码看看能不能得出这个论断:

@Override
protected void startInternal() throws LifecycleException {taskqueue = new TaskQueue(maxQueueSize);
        TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
        executor.setThreadRenewalDelay(threadRenewalDelay);
        if (prestartminSpareThreads) {executor.prestartAllCoreThreads();
        }
        taskqueue.setParent(executor);

        setState(LifecycleState.STARTING);
 }

你说的那个线程池在 StandardThreadExecutor 这个类的 startInternal 外面被初始化,咱们看看有没有什么生面孔,恐怕惟一的生面孔就是这个 TaskQueue, 咱们简略的看下这个队列。从源码外面咱们能够看进去,这个类继承了 LinkedBlockingQueue,咱们重点看入队和出队的办法

@Override
public boolean offer(Runnable o) {
  //we can't do any checks
    if (parent==null) {return super.offer(o);
    }
    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) {return super.offer(o);
    }
    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) {return super.offer(o);
    }
    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) {return false;}
    //if we reached here, we need to add it to the queue
    return super.offer(o);
}
  @Override
 public Runnable poll(long timeout, TimeUnit unit)
            throws InterruptedException {Runnable runnable = super.poll(timeout, unit);
        if (runnable == null && parent != null) {
            // the poll timed out, it gives an opportunity to stop the current
            // thread if needed to avoid memory leaks.
            parent.stopCurrentThreadIfNeeded();}
        return runnable;
    }

    @Override
    public Runnable take() throws InterruptedException {if (parent != null && parent.currentThreadShouldBeStopped()) {return poll(parent.getKeepAliveTime(TimeUnit.MILLISECONDS),
                    TimeUnit.MILLISECONDS);
            // yes, this may return null (in case of timeout) which normally
            // does not occur with take()
            // but the ThreadPoolExecutor implementation allows this
        }
        return super.take();}

通过上文咱们能够晓得,如果在线程池的线程数量和最大线程数相等,才会入队。以后未实现的工作小于以后线程池的线程数目也会入队。如果以后线程池的线程数目小于最大线程数,入队失败返回 false。Tomcat 的 ThreadPoolExecutor 继承了 JDK 的线程池,但在执行工作的时候仍然调用的是父类的办法,看上面的代码:

  public void execute(Runnable command, long timeout, TimeUnit unit) {submittedCount.incrementAndGet();
        try {super.execute(command);
        } catch (RejectedExecutionException rx) {if (super.getQueue() instanceof TaskQueue) {final TaskQueue queue = (TaskQueue)super.getQueue();
                try {if (!queue.force(command, timeout, unit)) {submittedCount.decrementAndGet();
                        throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                    }
                } catch (InterruptedException x) {submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {submittedCount.decrementAndGet();
                throw rx;
            }

        }
   }

所以咱们还是要进 JDK 的线程池看这个 execute 办法是怎么执行的:

 public void execute(Runnable command) {if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))
                return;
            c = ctl.get();}
        if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

这个代码也比拟直观,不如你提交了一个 null 值,抛空指针异样。而后判断以后线程池的线程数是否小于外围线程数,小于则增加线程。如果不小于外围线程数,判断以后线程池是否还在运行,如果还在运行,就尝试将工作增加进队列,走到这个判断阐明以后线程池的线程曾经达到外围线程数,然而还小于最大线程数,而后 TaskQueue 返回 false,就接着向线程池增加线程。那么当初整个 Tomcat 解决申请的流程,咱们心里就大抵无数了,当初我想问一个问题,当初已知的是,我能够认为执行咱们 controller 办法的是线程池的线程,然而如果办法外面执行工夫比拟长,那么线程池的线程就会始终被占用,咱们的零碎当初随着业务的增长刚好面临着这样的问题,一些文件上传碰上流量高峰期,就会始终占用这个线程,导致整个零碎处于一种不可用的状态。请问该如何解决?

小陈道:

通过异步能够解决嘛,就是将这类工作进行隔离,碰上这类工作先进行返回,等到执行结束再给响应?我的意思是说应用线程池。

面试官道:

但用户怎么晓得我上传的图片是否胜利呢,你返回的后果是什么呢,是未知,而后让用户过几分钟再看看上传后果?这看起来有些不敌对哦。你能剖析一下外围问题在哪里嘛?

小陈陷入了深思,想了一会说道:

是的,您说的对,这的确有些不敌对,我想外围问题还是开释执行 controller 层办法线程,同时放弃 TCP 连贯。

面试官点了拍板:

还能够,其实这个能够通过异步 Servlet 来解决,Servlet 3.0 引入了异步 Servlet,解决了咱们下面的问题,咱们能够将这种工作专门交付给一个线程池解决的共事,也放弃着本来的 HTTP 连贯。具体的应用如下:

@WebServlet(urlPatterns = "/asyncServlet",asyncSupported = true)
public class AsynchronousServlet extends HttpServlet {private static final ExecutorService BIG_FILE_POOL = Executors.newFixedThreadPool(10);

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {AsyncContext asyncContext = req.startAsync(req,resp);
        BIG_FILE_POOL.submit(()->{
            try {TimeUnit.SECONDS.sleep(10);
                ServletOutputStream outputStream = resp.getOutputStream();
                outputStream.write("task complete".getBytes(StandardCharsets.UTF_8));
                outputStream.flush();} catch (Exception e) {e.printStackTrace();
            }
            asyncContext.complete();});
    }
}

在 Spring MVC 下 该如何应用呢, Spring MVC 对异步 Servlet 进行了封装,只须要返回 DeferredResult,就能简便的应用异步 Servlet:

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {DeferredResult<String> deferredResult = new DeferredResult<String>();
  // Add deferredResult to a Queue or a Map...
  return deferredResult;
}
// In some other thread...
deferredResult.setResult(data);
// Remove deferredResult from the Queue or Map
@RestController
public class AsyncTestController {private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4,4,4L,TimeUnit.SECONDS,new LinkedBlockingQueue<>());

    @GetMapping("/asnc")
    public DeferredResult<String> pictureUrl(){DeferredResult<String> deferredResult = new DeferredResult<>();
        threadPoolExecutor.execute(()->{
            try {
                // 模仿耗时操作
                TimeUnit.SECONDS.sleep(6);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            deferredResult.setResult("hello world");
        });
        return deferredResult;
    }
}

哈哈哈哈,感觉不是面试,感觉我在给你上课一样。我对你的感觉还能够,等二面吧。

小陈:

啊,好的。

写在最初

写本文的时候用到了 chatGPT 来查资料,然而 chatGPT 给的材料存在很多谬误,chatGPT 呈现了认知偏差,比方将 Jetty 解决申请流程当成了 Tomcat 解决申请的流程,更细一点感觉还是没方法答复进去。还是要本人去看的。

参考资料

  • Java NIO 浅析 https://zhuanlan.zhihu.com/p/23488863
  • 深度解读 Tomcat 中的 NIO 模型 https://klose911.github.io/html/nio/tomcat.html
  • Tomcat – maxThreads vs. maxConnections https://stackoverflow.com/questions/24678661/tomcat-maxthreads-vs-maxconnections
  • 从一次线上问题说起,详解 TCP 半连贯队列、全连贯队列 https://developer.aliyun.com/article/804896
  • 就是要你懂 TCP– 半连贯队列和全连贯队列 https://plantegg.github.io/2017/06/07/%E5%B0%B1%E6%98%AF%E8%A…
  • Tomcat 配置文档 https://tomcat.apache.org/tomcat-8.5-doc/config/http.html
  • Java NIO 系列文章之 浅析 Reactor 模式 https://pjmike.github.io/2018/09/20/Java-NIO-%E7%B3%BB%E5%88%…
  • Java NIO – non-blocking channels vs AsynchronousChannels https://stackoverflow.com/questions/22177722/java-nio-non-blocking-channels-vs-asynchronouschannels
  • asynchronous and non-blocking calls? also between blocking and synchronous https://stackoverflow.com/questions/2625493/asynchronous-and-non-blocking-calls-also-between-blocking-and-synchronous
  • Java AIO 源码解析 https://cdf.wiki/posts/2976168065/
  • 每天都在用,但你晓得 Tomcat 的线程池有多致力吗?https://www.cnblogs.com/thisiswhy/p/12782548.html
  • 异步 Servlet 在转转图片服务的实际 https://juejin.cn/post/7124116514382774286
正文完
 0