文章编号:11568时间:2024-10-01人气:
Tomcat 是一个免费且开源的 Java Servlet 容器,可用于部署 Java EE Web 应用程序。Tomcat 7.0 是一个长期支持(LTS)版本,自 2011 年以来一直受到支持,并且仍然被广泛使用。
获取 Tomcat 7.0 的最简单方法是直接从 Apache Tomcat 网站下载。以下是下载步骤:
Tomcat 使用线程池来处理传入的请求。线程池是由 ExecutorService 对象表示的。要获得 Tomcat 线程池对象,请执行以下步骤:
import org.apache.catalina.connector.Connector;
Connector connector = (Connector) context.getConnector();
ExecutorService executorService = connector.getExecutorService();
现在,您拥有了 Tomcat 线程池对象,可以使用它来控制线程池的行为,例如调整线程数、任务队列大小等。
获取 Tomcat 7.0 并获取其线程池对象是一个快速且简单的过程。通过遵循这些步骤,您可以轻松地设置 Tomcat 环境并开始部署 Java EE Web 应用程序。
在程序中,我们会用各种池化技术来缓存创建昂贵的对象,比如线程池、连接池、内存池。 一般是预先创建一些对象放入池中,使用的时候直接取出使用,用完归还以便复用,还会通过一定的策略调整池中缓存对象的数量,实现池的动态伸缩。
由于线程的创建比较昂贵,随意、没有控制地创建大量线程会造成性能问题,因此短平快的任务一般考虑使用线程池来处理,而不是直接创建线程。 但是在使用线程池的时候应该注意线程池的使用,如果使用不当,将会导致生产事故。
一、线程池的声明需要手动进行Java 中的 Executors 类定义了一些快捷的工具方法,来帮助我们快速创建线程池。 《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 new ThreadPoolExecutor 来创建线程池。 这一条规则的背后,是大量血淋淋的生产事故,最典型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因为资源耗尽导致 OOM 问题。
首先,我们来看一下 newFixedThreadPool 为什么可能会出现 OOM 的问题。我们写一段测试代码,来初始化一个单线程的 FixedThreadPool,循环 1 亿次向线程池提交任务,每个任务都会创建一个比较大的字符串然后休眠一小时:
@GetMapping(oom1)public void oom1() throws InterruptedException {ThreadPoolExecutor threadPool = (ThreadPoolExecutor) (1);//打印线程池的信息,稍后我会解释这段代码printStats(threadPool); for (int i = 0; i < ; i++) {(() -> {String payload = (1, )(__ -> a)(()) + ()();try {(1);} catch (InterruptedException e) {}(payload);});}();(1, );}执行程序后不久,日志中就出现了如下 OOM:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded翻看 newFixedThreadPool 方法的源码不难发现,线程池的工作队列直接 new 了一个 LinkedBlockingQueue,而默认构造方法的 LinkedBlockingQueue 是一个 _VALUE 长度的队列,可以认为是无界的:
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, ,new LinkedBlockingQueue虽然使用 newFixedThreadPool 可以把工作线程控制在固定的数量上,但任务队列是无界的。 如果任务较多并且执行较慢的话,队列可能会快速积压,撑爆内存导致 OOM。
我们再把刚才的例子稍微改一下,改为使用 newCachedThreadPool 方法来获得线程池。程序运行不久后,同样看到了如下 OOM 异常:
[11:30:30.487] [http-nio--exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - () for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is : unable to create new native thread] with root : unable to create new native thread从日志中可以看到,这次 OOM 的原因是无法创建线程,翻看 newCachedThreadPool 的源码可以看到,这种线程池的最大线程数是 _VALUE,可以认为是没有上限的,而其工作队列 SynchronousQueue 是一个没有存储空间的阻塞队列。 这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的。
由于我们的任务需要 1 小时才能执行完成,大量的任务进来后会创建大量的线程。我们知道线程是需要分配一定的内存空间作为线程栈的,比如 1MB,因此无限制创建线程必然会导致 OOM:
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, _VALUE,60L, ,new SynchronousQueue其实,大部分 Java 开发同学知道这两种线程池的特性,只是抱有侥幸心理,觉得只是使用线程池做一些轻量级的任务,不可能造成队列积压或开启大量线程。
但,现实往往是残酷的。 我之前就遇到过这么一个事故:用户注册后,我们调用一个外部服务去发送短信,发送短信接口正常时可以在 100 毫秒内响应,TPS 100 的注册量,CachedThreadPool 能稳定在占用 10 个左右线程的情况下满足需求。 在某个时间点,外部短信服务不可用了,我们调用这个服务的超时又特别长,比如 1 分钟,1 分钟可能就进来了 6000 用户,产生 6000 个发送短信的任务,需要 6000 个线程,没多久就因为无法创建线程导致了 OOM,整个应用程序崩溃。
因此,我同样不建议使用 Executors 提供的两种快捷的线程池,原因如下:
我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数。
任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。 当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。 此时,有意义的线程名称,就可以方便我们定位问题。
除了建议手动声明线程池以外,我还建议用一些监控手段来观察线程池的状态。 线程池这个组件往往会表现得任劳任怨、默默无闻,除非是出现了拒绝策略,否则压力再大都不会抛出一个异常。 如果我们能提前观察到线程池队列的积压,或者线程数量的快速膨胀,往往可以提早发现并解决问题。
二、线程池线程管理策略详解在之前的 Demo 中,我们用一个 printStats 方法实现了最简陋的监控,每秒输出一次线程池的基本内部信息,包括线程数、活跃线程数、完成了多少任务,以及队列中还有多少积压任务等信息:
private void printStats(ThreadPoolExecutor threadPool) { ()(() -> {(=========================);(Pool Size: {}, ());(Active Threads: {}, ());(Number of Tasks Completed: {}, ());(Number of Tasks in Queue: {}, ()());(=========================);}, 0, 1, );}接下来,我们就利用这个方法来观察一下线程池的基本特性吧。
首先,自定义一个线程池。 这个线程池具有 2 个核心线程、5 个最大线程、使用容量为 10 的 ArrayBlockingQueue 阻塞队列作为工作队列,使用默认的 AbortPolicy 拒绝策略,也就是任务添加到线程池失败会抛出 RejectedExecutionException。 此外,我们借助了 Jodd 类库的 ThreadFactoryBuilder 方法来构造一个线程工厂,实现线程池线程的自定义命名。
@GetMapping(right)public int right() throws InterruptedException {//使用一个计数器跟踪完成的任务数AtomicInteger atomicInteger = new AtomicInteger();//创建一个具有2个核心线程、5个最大线程,使用容量为10的ArrayBlockingQueue阻塞队列作为工作队列的线程池,使用默认的AbortPolicy拒绝策略ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5,5, ,new ArrayBlockingQueue<>(10),new ThreadFactoryBuilder()(demo-threadpool-%d)(),new ());printStats(threadPool);//每隔1秒提交一次,一共提交20次任务(1, 20)(i -> {try {(1);} catch (InterruptedException e) {();}int id = ();try {(() -> {({} started, id);//每个任务耗时10秒try {(10);} catch (InterruptedException e) {}({} finished, id);});} catch (Exception ex) {//提交出现异常的话,打印出错信息并为计数器减一(error submitting task {}, id, ex);();}});(60);return ();}60 秒后页面输出了 17,有 3 次提交失败了
并且日志中也出现了 3 次类似的错误信息:
[14:24:52.879] [http-nio--exec-1] [ERROR] [:103 ] - error submitting task : Task [email protected] rejected from [email protected][Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 2]我们把 printStats 方法打印出的日志绘制成图表,得出如下曲线:
至此,我们可以总结出线程池默认的工作行为:
不会初始化 corePoolSize 个线程,有任务来了才创建工作线程;
当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中;
当工作队列满了后扩容线程池,一直到线程个数达到 maximumPoolSize 为止;
如果队列已满且达到了最大线程后还有任务进来,按照拒绝策略处理;
当线程数大于核心线程数时,线程等待 keepAliveTime 后还是没有任务需要处理的话,收缩线程到核心线程数。
了解这个策略,有助于我们根据实际的容量规划需求,为线程池设置合适的初始化参数。当然,我们也可以通过一些手段来改变这些默认工作行为,比如:
声明线程池后立即调用 prestartAllCoreThreads 方法,来启动所有核心线程;
传入 true 给 allowCoreThreadTimeOut 方法,来让线程池在空闲的时候同样回收核心线程。
不知道你有没有想过:Java 线程池是先用工作队列来存放来不及处理的任务,满了之后再扩容线程池。 当我们的工作队列设置得很大时,最大线程数这个参数显得没有意义,因为队列很难满,或者到满的时候再去扩容线程池已经于事无那么,我们有没有办法让线程池更激进一点,优先开启更多的线程,而把队列当成一个后备方案呢?比如我们这个例子,任务执行得很慢,需要 10 秒,如果线程池可以优先扩容到 5 个最大线程,那么这些任务最终都可以完成,而不会因为线程池扩容过晚导致慢任务来不及处理。
这里我只给你一个大致思路:
由于线程池在工作队列满了无法入队的情况下会扩容线程池,那么我们是否可以重写队列的 offer 方法,造成这个队列已满的假象呢?
由于我们 Hack 了队列,在达到了最大线程后势必会触发拒绝策略,那么能否实现一个自定义的拒绝策略处理程序,这个时候再把任务真正插入队列呢?
接下来,就请你动手试试看如何实现这样一个“弹性”线程池吧。 Tomcat 线程池也实现了类似的效果,可供你借鉴。
三、线程池本身是不是复用的不久之前我遇到了这样一个事故:某项目生产环境时不时有报警提示线程数过多,超过 2000 个,收到报警后查看监控发现,瞬时线程数比较多但过一会儿又会降下来,线程数抖动很厉害,而应用的访问量变化不大。
为了定位问题,我们在线程数比较高的时候进行线程栈抓取,抓取后发现内存中有 1000 多个自定义线程池。 一般而言,线程池肯定是复用的,有 5 个以内的线程池都可以认为正常,而 1000 多个线程池肯定不正常。
在项目代码里,我们没有搜到声明线程池的地方,搜索 execute 关键字后定位到,原来是业务代码调用了一个类库来获得线程池,类似如下的业务代码:调用 ThreadPoolHelper 的 getThreadPool 方法来获得线程池,然后提交数个任务到线程池处理,看不出什么异常。
@GetMapping(wrong)public String wrong() throws InterruptedException {ThreadPoolExecutor threadPool = ();(1, 10)(i -> {(() -> { {(1);} catch (InterruptedException e) {}});});return OK;}但是,来到 ThreadPoolHelper 的实现让人大跌眼镜,getThreadPool 方法居然是每次都使用 来创建一个线程池。
class ThreadPoolHelper {public static ThreadPoolExecutor getThreadPool() {//线程池没有复用return (ThreadPoolExecutor) ();}}我们可以想到 newCachedThreadPool 会在需要时创建必要多的线程,业务代码的一次业务操作会向线程池提交多个慢任务,这样执行一次业务操作就会开启多个线程。 如果业务操作并发量较大的话,的确有可能一下子开启几千个线程。
那,为什么我们能在监控中看到线程数量会下降,而不会撑爆内存呢?
回到 newCachedThreadPool 的定义就会发现,它的核心线程数是 0,而 keepAliveTime 是 60 秒,也就是在 60 秒之后所有的线程都是可以回收的。 好吧,就因为这个特性,我们的业务程序死得没太难看。
要修复这个 Bug 也很简单,使用一个静态字段来存放线程池的引用,返回线程池的代码直接返回这个静态字段即可。 这里一定要记得我们的最佳实践,手动创建线程池。 修复后的 ThreadPoolHelper 类如下:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded0四、仔细斟酌线程池的混用策略线程池的意义在于复用,那这是不是意味着程序应该始终使用一个线程池呢?要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列:对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列。 而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数2倍(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。
业务代码使用了线程池异步处理一些内存中的数据,但通过监控发现处理得非常慢,整个处理过程都是内存中的计算不涉及 IO 操作,也需要数秒的处理时间,应用程序 CPU 占用也不是特别高,有点不可思议。
经排查发现,业务代码使用的线程池,还被一个后台的文件批处理任务用到了。或许是够用就好的原则,这个线程池只有 2 个核心线程,最大线程也是 2,使用了容量为 100 的 ArrayBlockingQueue 作为工作队列,使用了 CallerRunsPolicy 拒绝策略:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded1这里模拟一下文件批处理的代码,在程序启动后通过一个线程开启死循环逻辑,不断向线程池提交任务,任务的逻辑是向一个文件中写入大量的数据:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded2可以想象到,这个线程池中的 2 个线程任务是相当重的。通过 printStats 方法打印出的日志,我们观察下线程池的负担:
可以看到,线程池的 2 个线程始终处于活跃状态,队列也基本处于打满状态。 因为开启了 CallerRunsPolicy 拒绝处理策略,所以当线程满载队列也满的情况下,任务会在提交任务的线程,或者说调用 execute 方法的线程执行,也就是说不能认为提交到线程池的任务就一定是异步处理的。 如果使用了 CallerRunsPolicy 策略,那么有可能异步任务变为同步执行。 从日志的第四行也可以看到这点。 这也是这个拒绝策略比较特别的原因。
不知道写代码的同学为什么设置这个策略,或许是测试时发现线程池因为任务处理不过来出现了异常,而又不希望线程池丢弃任务,所以最终选择了这样的拒绝策略。 不管怎样,这些日志足以说明线程池是饱和状态。
可以想象到,业务代码复用这样的线程池来做内存计算,命运一定是悲惨的。我们写一段代码测试下,向线程池提交一个简单的任务,这个任务只是休眠 10 毫秒没有其他逻辑:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded3我们使用 wrk 工具对这个接口进行一个简单的压测,可以看到 TPS 为 75,性能的确非常差。
细想一下,问题其实没有这么简单。 因为原来执行 IO 任务的线程池使用的是 CallerRunsPolicy 策略,所以直接使用这个线程池进行异步计算的话,当线程池饱和的时候,计算任务会在执行 Web 请求的 Tomcat 线程执行,这时就会进一步影响到其他同步处理的线程,甚至造成整个应用程序崩溃。
解决方案很简单,使用独立的线程池来做这样的“计算任务”即可。计算任务打
本文详细介绍了Netty网络编程框架的核心概念以及入门案例。
1Netty的介绍基于事件驱动的JavaNIO网络通信框架,可以快速简单地开发网络应用程序。
极大地简化并优化了TCP和UDP套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
支持多种通信协议如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议,同样支持自定义协议。
简单的说,Netty有三个优点:
高并发:基于NIO开发(Reactor模型),并发性能相比BIO得到了很大提高。
传输快:传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,使用高性能序列化协议protobuf,实现了高效传输。
封装好:封装了原始NIO编程的很多细节,提供了易于使用调用接口,使用更简单。
借用官方的描述:Netty成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
Netty的社区目前非常活跃。 很多涉及到网络调用的开源项目和框架底层都用到了Netty,比如我们常用的Dubbo、RocketMQ、Elasticsearch、gRPC、Spark、GateWay等等。
总之,涉及到网络编程开发时,比如即时通讯系统、自定义RPC框架、自定义HTTP服务器、实时消息推送系统等场景下,用Netty,准没错。
2Netty的核心组件2.1Channel通道,Netty网络操作抽象类,包括基本的I/O操作,如bind、connect、read、write等,Netty的Channel接口所提供的API,大大地降低了直接使用Socket类的复杂性。
不同协议、不同的阻塞类型的连接都有不同的Channel类型与之对应,下面是一些常用的Channel类型:
NioSocketChannel,异步的客户端TCPSocket连接。
NioServerSocketChannel,异步的服务器端TCPSocket连接。
NioDatagramChannel,异步的UDP连接。
NioSctpChannel,异步的客户端Sctp连接。
NioSctpServerChannel,异步的Sctp服务器端连接这些通道涵盖了UDP和TCP网络IO以及文件IO。
2.2EventLoopEventLoop(事件循环)接口是Netty的核心接口,用于处理连接的生命周期中所发生的各种事件,实际上就是负责监听网络事件并调用事件处理器进行相关I/O操作的处理。
EventLoop内部持有NIO中的Selector,Channel将会注册到EventLoop中,一个EventLoop可以监听多个Channel,EventLoop是实现IO多路复用的核心,可以看作是Reactor模型中的mainReactor。
Channel为Netty网络操作抽象类,EventLoop负责监听注册到其上的Channel的IO事件,两者配合完成I/O操作。
2.3ChannelFuture在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理。
Channel会注册到EventLoop中后会立即返回一个ChannelFuture对象,可以通过ChannelFuture#addListener注册GenericFutureListener监听器,当操作执行成功或失败时监听会自动触发注册的监听事件。
2.4ChannelHandlerChannelHandler是消息的具体处理器。 他负责处理各种任务,这个任务非常广泛,可以是读写事件、连接、解码编码、数据转换、业务逻辑等等,处理完毕之后将数据继续转发到ChannelPipeline中的下一个ChannelHandler。
通过定制ChannelHandler可对Netty进行扩展。ChannelHandler接口本身并没有提供很多方法,因为这个接口有许多的方法需要实现,为了方便使用,可以继承它的子类:
ChannelInboundHandler用于处理入站I/O事件
ChannelOutboundHandler用于处理出站I/O操作
或者使用以下适配器类,更加方便:
ChannelInboundHandlerAdapter用于处理入站I/O事件
ChannelOutboundHandlerAdapter用于处理出站I/O操作
ChannelDuplexHandler用于处理入站和出站事件
2.5ChannelPipelineChannelPipeline是一个ChannelHandler的链表,即ChannelHandler组成的List,提供了一个沿着链传播入站和出站事件流的API。
可以在ChannelPipeline上通过addLast()方法添加一个或者多个ChannelHandler,因为一个数据或者事件可能会被多个Handler处理。 当一个ChannelHandler处理完之后就将数据交给下一个ChannelHandler。
在执行时,入站事件会从链表head往后传递到最后一个入站的handler(ChannelInboundHandler类型),出站事件会从链表tail往前传递到最前一个出站的handler(ChannelOutboundHandler类型),两种类型的handler在执行时互不干扰。 如果Handler同时属于入站、出站Handler,则都会执行一次。
在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应,当Channel被创建时,它会被自动地分配到它专属的ChannelPipeline。
2.5.1ChannelHandlerContext用于传输业务数据,保存Channel相关的所有上下文信息。
将Handler和Pipeline联系起来,实际上ChannelPipeline中直接存储的是ChannelHandlerContext,而每个ChannelHandlerContext中又关联着唯一一个ChannelHandler。
2.5.2入站和出站数据入站,一般是指读事件触发,即数据要读进来;数据从底层的JavaNIOchannel读取到Netty的Channel,此过程中会进行数据解码。
数据出站,一般是指写事件触发,即数据要写出去;数据从Netty的Channel写入底层的JavaNIOchanel,此过程中会进行数据编码。
入站会从先读取数据,再执行入站的Handler;出站会先执行出站的Handler,再写入。
即每次出现读事件时,会执行入站操作,实际读取数据之后,会先从头至尾依次调用ChannelPipeline中的InboundHandler处理,不会调用OutboundHandler;而触发写事件时,会执行出站操作,实际写入数据之前,则会从尾到头依次调用ChannelPipeline的OutboundHandler处理,不会调用InboundHandler;
下图描述了ChannelPipeline中的ChannelHandlers通常如何处理I/O事件():
入站事件由入站处理程序按自下而上的方向处理,如图左侧所示。 入站处理程序通常处理由图底部的I/O线程生成的原始入站数据,例如通过(ByteBuffer)读取。
出站事件由出站处理程序按自上而下的方向处理,如图右侧所示。 出站处理程序通常会生成或转换出站流量,例如写入请求。 如果出站事件超出了底部出站处理程序,则由与通道关联的I/O线程处理。 I/O线程执行实际的输出操作,例如通过(ByteBuffer)输出。
2.6EventLoopGroupEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环EventLoop,EventLoop的主要作用实际就是负责监听网络事件并调用事件处理器进行相关I/O操作的处理。
EventLoopGroup内部的每个EventLoop通常包含1个Selector和1个事件循环线程,一个EventLoop可以绑定多个Channel,但一个Channel只能绑定一个EventLoop,这样某一个连接的IO事件就在专有的线程上处理,保证线程安全。
NettyServer端包含1个BossNioEventLoopGroup和1个WorkerNioEventLoopGroup:
BossNioEventLoop主要循环执行的工作:
select监听accept事件。
处理到来的accept事件,与Client建立连接,生成SocketChannel,并将SocketChannel注册到某个WorkerNioEventLoop的Selector上。
处理任务队列中的任务,runAllTasks。 任务队列中的任务包括用户调用或schedule执行的任务,或者其它线程提交到该eventloop的任务。
WorkerNioEventLoop主要循环执行的工作:
select监听read、write事件。
处理到来的read、write事件,在NioSocketChannel可读、可写事件发生时进行处理。
处理任务队列中的任务,runAllTasks。
3Netty的线程模型Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,就会建立连接并把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,以及业务逻辑,这些都由对应的Handler处理。
Netty主要靠NioEventLoopGroup线程池的配置来实现具体的线程模型。
3.1单线程模型bossGroup和workerGroup使用同一个NioEventLoopGroup,且配置线程数为1。
适合连接量和并发量都不大的应用。
3.2多线程模型bossGroup和workerGroup使用不同NioEventLoopGroup,且bossGroup配置线程数为1。
适合连接量不大,并发量大的应用。
3.3主从多线程模型bossGroup和workerGroup使用不同NioEventLoopGroup,且都配置为多线程。
适合连接量和并发量都比较大的应用。
从一个主线程NIO线程池中选择一个线程作为Acceptor线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。 连接建立完成后分派给workerGroup线程。
4Netty默认启动的线程数*EventLoopGroup默认的构造函数实际会起的线程数为CPU核心数2,但bossGroup一般设置数量为1。 EventLoopGroup内部的EventLoop数量就是线程数量,保证1对1的关系。 **
5Netty的启动过程5.1服务端首先初始化两个NioEventLoopGroup,其中boosGroup用于处理客户端建立TCP连接的请求(Accept事件),workerGroup用于处理每一条连接的I/O读写事件和具体的业务逻辑。
NioEventLoopGroup类的无参构造函数的默认设置的线程数量是CPU核心数2。 一般情况下我们会指定bossGroup的线程数为1(并发连接量不大的时候),workGroup的线程数量为CPU核心数2。
随后创建一个ServerBootstrap,它是服务端的启动引导类/辅助类,它将引导我们进行服务端的启动工作。 通过ServerBootstrap配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler等。
最后通过bind()方法绑定端口,开始工作。
publicclassNettyServer{staticintport=8888;publicstaticvoidmain(String[]args){//1bossGroup用于接收连接mainReactor//workerGroup用于具体的处理subReactorEventLoopGroupbossGroup=newNioEventLoopGroup(1);EventLoopGroupworkerGroup=newNioEventLoopGroup();try{//2.创建服务端启动引导/辅助类:ServerBootstrapServerBootstrapserverBootstrap=newServerBootstrap();//3.给引导类配置两大线程组,确定了线程模型(bossGroup,workerGroup)//4.指定IO模型()(newChannelInitializer首先初始化一个NioEventLoopGroup。
随后创建一个Bootstrap,它是客户端的启动引导类/辅助类,它将引导我们进行客户端的启动工作。 通过Bootstrap配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler等。
最后通过connect()方法使用服务端的ip和port进行连接,开始工作。
publicclassNettyClient{staticintport=8888;staticStringhost=127.0.0.1;publicstaticvoidmain(String[]args){//1.创建一个NioEventLoopGroup对象实例EventLoopGroupgroup=newNioEventLoopGroup();try{//2.创建客户端启动引导/辅助类:BootstrapBootstrapbootstrap=newBootstrap();//3.指定线程组(group)//4.指定IO模型()(newChannelInitializerTCP是以流的方式来处理数据,底层会有一个缓冲区,一个完整的较大的包可能会被TCP拆分成多个包进行发送,也可能把多个小的包封装成一个大的数据包发送。
TCP粘包/拆包的原因:应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,实际表现就是不能收到完整的消息。 而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象,实际表现就是一次性收到多条粘连在一起消息。
报头的选项字段有MSS(MaximumSegmentSize,最大报文段大小)字段,规定一个TCP包最大可传输的字节数,一般是1500-20-20=1460字节,大于该大小时将发生拆包。
6.2解决办法使用Netty自带的解码器
LineBasedFrameDecoder:发送端发送数据包的时候,每个数据包之间以换行符作为分隔,即\n或者\r\n,其工作原理是它依次遍历ByteBuf中的可读字节,判断是否有换行符,然后进行相应的截取。
DelimiterBasedFrameDecoder:可以自定义分隔符解码器,其实际上是一种特殊的DelimiterBasedFrameDecoder解码器。
FixedLengthFrameDecoder:固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。 需要约定每一个包的固定大小。
LengthFieldBasedFrameDecoder:将消息分为消息头和消息体。 在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息。
通过自定义协议进行粘包和拆包的处理。
7Netty的长连接、心跳机制Netty客户端和服务器采用长连接保持联系。 client与server完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。 长连接的可以省去较多的TCP建立和关闭的操作,降低对网络资源的依赖,节约时间。
在TCP保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候,client与server之间如果没有交互的话,它们是无法发现对方已经掉线的。 为了解决这个问题,我们就需要引入心跳机制。
心跳机制的工作原理是:在client与server之间在一定时间内没有数据交互时,即处于idle状态时,客户端或服务器就会发送一个特殊的数据包给对方,当接收方收到这个数据报文后,也立即发送一个特殊的数据报文,回应发送方,此即一个PING-PONG交互。 所以,当某一端收到心跳消息后,就知道了对方仍然在线,这就确保TCP连接的有效性。
TCP实际上自带的就有长连接选项,本身是也有心跳包机制,也就是TCP的选项:SO_KEEPALIVE。 但是,TCP协议层面的长连接灵活性不够。 所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在Netty层面通过编码实现。 通过Netty实现心跳机制的话,核心类是IdleStateHandler。
Netty支持的哪些心跳类型设置:
readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。
writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。
allIdleTime:所有类型的超时时间。
8Netty的零拷贝零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。 这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
Netty中的零拷贝体现在以下几个方面:
Netty提供了CompositeByteBuf类,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的数据拷贝。
ByteBuf支持slice操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。
通过FileRegion包装的实现文件传输,可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
Netty的接收和发送ByteBuffer采用DIRECTBUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。 如果使用传统的堆内存(HEAPBUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。 相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
9Netty和Tomcat的区别作用不同:Tomcat是Servlet容器,可以视为Web服务器,是一款已经开发好的软件,而Netty是一款强大的异步事件驱动的网络应用程序框架,用于简化网络编程,可用于编写各种服务器。
协议不同:Tomcat是基于http协议的Web服务器,而Netty支持各种现成的协议并且能通过编程自定义各种协议,因为Netty本身自己能编码/解码字节流,所以Netty可以实现HTTP服务器、FTP服务器、UDP服务器、RPC服务器、WebSocket服务器、Redis的Proxy服务器、MySQL的Proxy服务器等等。
10Netty简单案例 publicclassNettyClient{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{//1.创建一个NioEventLoopGroup对象实例EventLoopGroupgroup=newNioEventLoopGroup();try{//2.创建客户端启动引导/辅助类:BootstrapBootstrapbootstrap=newBo在Web2.0的浪潮中,各种页面技术和框架不断涌现,为服务器端的基础架构提出了更高的稳定性和可扩展性的要求。 近年来,作为开源中间件的全球领导者,JBoss在J2EE应用服务器领域已成为发展最为迅速的应用服务器。 在市场占有率和服务满意度上取得了巨大的成功,丝毫不逊色于其它的非开源竞争对手,如WebSphere、WebLogic、Application Server。 JBoss Web的诸多优越性能,正是其广为流行的原因。 基于Tomcat内核,青胜于蓝Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可。 其运行时占用的系统资源小,扩展性好,且支持负载平衡与邮件服务等开发应用系统常用的功能。 作为一个小型的轻量级应用服务器,Tomcat在中小型系统和并发访问用户不是很多的场合下被普遍使用,成为目前比较流行的Web 应用服务器。 而JBoss Web采用业界最优的开源Java Web引擎, 将Java社区中下载量最大,用户数最多,标准支持最完备的Tomcat内核作为其Servlet容器引擎,并加以审核和调优。 单纯的Tomcat性能有限,在很多地方表现有欠缺,如活动连接支持、静态内容、大文件和HTTPS等。 除了性能问题,Tomcat的另一大缺点是它是一个受限的集成平台,仅能运行Java应用程序。 企业在使用时Tomcat,往往还需同时部署Apache Web Server以与之整合。 此配置较为繁琐,且不能保证性能的优越性。 JBoss在Tomcat的基础上,对其进行本地化,将Tomcat 以内嵌的方式集成到 JBoss 中。 JBoss Web通过使用APR和Tomcat本地技术的混合模型来解决Tomcat的诸多不足。 混合技术模型从最新的操作系统技术里提供了最好的线程和事件处理。 结果,JBoss Web达到了可扩展性,性能参数匹配甚至超越了本地Apache HTTP服务器或者IIS。 譬如JBoss Web能够提供数据库连接池服务,不仅支持 JSP 等 Java 技术,同时还支持其他 Web 技术的集成,譬如 PHP、 两大阵营。 标准化是减小技术依赖风险,保护投资最好的方式。 JBoss Web率先支持全系列JEE Web标准,从根本上保证了应用“一次开发,到处运行”的特点,使应用成品能方便地在JBoss Web和其他Java Web服务器之间轻易迁移。 集多功能于一身,性能卓越作为Web 应用服务器中的明星产品,JBoss Web服务器集多种功能于一身。 其关键功能包括:完全支持Java EE、高度的扩展性、快速的静态内容处理、群集、OpenSSL、URL重写和综合性。 JBoss Web服务器具有原生特性和强大的可扩展性,可支持多种并非基于Java的服务器内容处理技术,可同时运行JSP, Servlet, Microsoft , PHP 及 CGI,为其提供一个单一的、高性能的企业级部署平台。 与Tomcat 相比,JBoss Web在静态资源访问方面性能优越。 JBoss Web支持两种组件模式——纯Java和Native I/O。 在Native组件的支持下,动态运行不会受到任何影响,而静态资源的访问利用了操作系统本身提供的0拷贝传送,CPU消耗降低,响应时间缩短,吞吐率大大提高,混合的连接模式支持最大达到个并发客户端的同时访问,与Apache Web服务器相当。 部署于高性能的操作系统,可利用JBoss Web对纯Java和Native I/O两种模式的支持,使得应用在开发时可随时跨平台敏捷迁移,而部署于高性能的操作系统相关的Native环境。 由于JBoss Web较好地解决了静态资源的访问性能问题,可在解决方案中把它直接作为强大的LVS的分发对象,和RHEL负载均衡系统结合,形成理论上无限线性扩展的负载均衡场景。 OpenSSL是业界最为快速和安全的开源传输组件,可借助操作系统和硬件的特性实现高效的安全承载。 JBoss Web集成了OpenSSL,可提供高效的安全传输服务,使得安全机制更上台阶。 研究表明, JBoss Web中的SSL性能比单纯的Tomcat快四倍。 URL重写功能可缩短URL,隐藏实际路径提高安全性,易于用户记忆和键入,及被搜索引擎收录。 Tomcat 不具备URL重写功能,JBoss Web则可提供一个灵活的URL rewriting操作引擎,支持无限个规则数和规则条件。 URL可被重写以支持遗留的URL错误处理,或应对服务器不时产生的其他问题。 JBoss Web既可单独运行,也可无缝嵌入JBoss应用服务器,成为JBoss中间件平台的一部分。 不仅后台服务调用的性能将得以提升,也可利用以下JBoss平台的特性提升Web应用功能:基于JGroups的多种集群方案的支持基于Arjuna技术的JTA和JTS的事务处理支持优化的线程池和连接池的支持基于JMX 控制台的基本管理支持和JBoss On的高级管理维护支持基于JBoss AOP技术的面向方面架构的支持Hibernate服务组件的支持专业团队支持业界大多数开源产品在技术方面富于创新性,但在可持续性,产品生命周期规划,以及质量保证方面缺乏有效保障,为软件集成商和最终用户所诟病。 红帽所力行的“专业化开源技术”则完美解决了这一问题。 来自开源社区的JBoss Web,在红帽专业化开源的锤炼下,在性能、扩展性、稳定性、安全性等方面,已成为一个达到企业级,甚至电信级标准的优秀产品。 红帽不仅有专职的技术团队投入JBoss Web的开发,而且具备专门的QA团队为产品作质量保证。 完善的集成测试和兼容性测试保证了JBoss Web自身的稳定性,并保证了它的后向兼容和其他JBoss产品协作良好的互操作性。 在服务体系保障方面,JBoss 开拓了以产品专家提供的专家级支持服务作为开源软件强大后盾的软件生态模式。 公司以及庞大的 JBoss 授权服务合作伙伴网络可为包括JBoss Web在内的整个JEMS 产品套件提供全面的支持服务。 与Tomcat相比,JBoss Web 可提供迁移服务与现场专家服务,在迁移服务方面,专家指导应用可从Tomcat向JBoss Web迁移,省时省力。 独特的服务订阅模式,全力保障软件生命周期,让企业高枕无忧。
通常提到线程安全问题等就有可能听到关线程安全和并发工具的一些片面的观点和结论。 比如“把 HashMap 改为 ConcurrentHashMap,就可以解决并发问题了呀”“要不我们试试无锁的 CopyOnWriteArrayList 吧,性能更好”。 的确,为了方便开发者进行多线程编程,现代编程语言会提供各种并发工具类。 但如果我们没有充分了解它们的使用场景、解决的问题,以及最佳实践的话,盲目使用就可能会导致一些坑,小则损失性能,大则无法确保多线程情况下业务逻辑正确性。
一、线程重用导致用户信息错乱的Bug之前有业务同学和我反馈,在生产上遇到一个诡异的问题,有时获取到的用户信息是别人的。 查看代码后,我发现他使用了ThreadLocal 来缓存获取到的用户信息。 我们知道,ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。 如果用户信息的获取比较昂贵(比如从数据库查询用户信息),那么在 ThreadLocal 中缓存数据是比较合适的做法。 但,这么做为什么会出现用户信息错乱的 Bug 呢?
使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。 在业务逻辑中,我先从 ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。
private static final ThreadLocal按理说,在设置用户信息之前第一次获取的值始终应该是 null,但我们要意识到,程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。 顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。 这时,ThreadLocal 中的用户信息就是其他用户的信息。
为了更快地重现这个问题,我在配置文件中设置一下 Tomcat 的参数,把工作线程池最大线程数设置为 1,这样始终是同一个线程在处理请求:
随后用户 2 来请求接口,这次就出现了 Bug,第一和第二次获取到用户 ID 分别是 1 和 2,如果是按照正常的来说数的应该是的null和2。 显然第一次获取到了用户 1 的信息,原因就是Tomcat 的线程池重用了线程。 从图中可以看到,两次请求的线程都是同一个线程:http-nio-8080-exec-1。
这个例子告诉我们,在写业务代码时,首先要理解代码会跑在什么线程上:
我们可能会抱怨学多线程没用,因为代码里没有开启使用多线程。 但其实,可能只是我们没有意识到,在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题。
因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。 这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。 如果在代码中使用了自定义的线程池,也同样会遇到这个问题。
理解了这个知识点后,我们修正这段代码的方案是,在代码的 finally 代码块中,显式清除 ThreadLocal 中的数据。 这样一来,新的请求过来即使使用了之前的线程也不会获取到错误的用户信息了。 修正后的代码如下:
@GetMapping(right)public Map right(@RequestParam(userId) Integer userId) {String before= ()() + : + ();(userId);try {String after = ()() + : + ();Map result = new HashMap();(before, before);(after, after);return result;} finally {//在finally代码块中删除ThreadLocal中的数据,确保数据不串();}}重新运行程序可以验证,再也不会出现第一次查询用户信息查询到之前用户请求的 Bug:
ThreadLocal 是利用独占资源的方式,来解决线程安全问题,那如果我们确实需要有资源在线程之间共享,应该怎么办呢?这时,我们可能就需要用到线程安全的容器了。
二、并发工具导致的线程安全问题JDK 1.5 后推出的 ConcurrentHashMap,是一个高性能的线程安全的哈希表容器。 “线程安全”这四个字特别容易让人误解,因为 ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。 我在相当多的业务代码中看到过这个误区,比如下面这个场景。 有一个含 900 个元素的 Map,现在再补充 100 个元素进去,这个补充操作由 10 个线程并发进行。 开发人员误以为使用了 ConcurrentHashMap 就不会有线程安全问题,于是不加思索地写出了下面的代码:在每一个线程的代码逻辑中先通过 size 方法拿到当前元素数量,计算 ConcurrentHashMap 目前还需要补充多少元素,并在日志中输出了这个值,然后通过 putAll 方法把缺少的元素添加进去。
//线程个数private static int THREAD_COUNT = 10;//总元素数量private static int ITEM_COUNT = 1000;//帮助方法,用来获得一个指定元素数量模拟数据的ConcurrentHashMapprivate ConcurrentHashMap从日志中可以看到:
初始大小 900 符合预期,还需要填充 100 个元素。
worker1 线程查询到当前需要填充的元素为 36,竟然还不是 100 的倍数。 worker13 线程查询到需要填充的元素数是负的,显然已经过度填充了。 最后 HashMap 的总项目数是 1536,显然不符合填充满 1000 的预期。
针对这个场景,我们可以举一个形象的例子。 ConcurrentHashMap 就像是一个大篮子,现在这个篮子里有 900 个桔子,我们期望把这个篮子装满 1000 个桔子,也就是再装 100 个桔子。 有 10 个工人来干这件事儿,大家先后到岗后会计算还需要补多少个桔子进去,最后把桔子装入篮子。
ConcurrentHashMap 这个篮子本身,可以确保多个工人在装东西进去时,不会相互影响干扰,但无法确保工人 A 看到还需要装 100 个桔子但是还未装的时候,工人 B 就看不到篮子中的桔子数量。 更值得注意的是,你往这个篮子装 100 个桔子的操作不是原子性的,在别人看来可能会有一个瞬间篮子里有 964 个桔子,还需要补 36 个桔子。
回到 ConcurrentHashMap,我们需要注意 ConcurrentHashMap 对外提供的方法或能力的限制:
使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。 因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。 显然,利用 size 方法计算差异值,是一个流程控制。 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。
@GetMapping(right)public String right() throws InterruptedException {ConcurrentHashMap可以看到,只有一个线程查询到了需要补 100 个元素,其他 9 个线程查询到不需要补元素,最后 Map 大小为 1000。 到了这里,你可能又要问了,使用 ConcurrentHashMap 全程加锁,还不如使用普通的 HashMap 呢。
其实不完全是这样。 ConcurrentHashMap 提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。 这就引申出代码中常见的另一个问题:在使用一些类库提供的高级工具类时,开发人员可能还是按照旧的方式去使用这些新类,因为没有使用其特性,所以无法发挥其威力。
三、并发工具的特性,导致性能降低问题我们来看一个使用 Map 来统计 Key 出现次数的场景吧,这个逻辑在业务代码中非常常见。
使用 ConcurrentHashMap 来统计,Key 的范围是 10。
使用最多 10 个并发,循环操作 1000 万次,每次操作累加随机的 Key。
如果 Key 不存在的话,首次设置值为 1。
//循环次数private static int LOOP_COUNT = ;//线程数量private static int THREAD_COUNT = 10;//元素数量private static int ITEM_COUNT = 10;private Map我们吸取之前的教训,直接通过锁的方式锁住 Map,然后做判断、读取现在的累计值、加 1、保存累加后值的逻辑。这段代码在功能上没有问题,但无法充分发挥 ConcurrentHashMap 的威力,改进后的代码如下:
private Map使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最后返回 Value。
由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。
这样在确保线程安全的情况下达到极致性能,把之前 7 行代码替换为了 1 行。
@GetMapping(good)public String good() throws InterruptedException {StopWatch stopWatch = new StopWatch();(normaluse);Map这段测试代码并无特殊之处,使用 StopWatch 来测试两段代码的性能,最后跟了一个断言判断 Map 中元素的个数以及所有 Value 的和,是否符合预期来校验代码的正确性。测试结果如下:
可以看到,优化后的代码,相比使用锁来操作 ConcurrentHashMap 的方式,性能提升了 10 倍。 你可能会问,computeIfAbsent 为什么如此高效呢?答案就在源码最核心的部分,也就是 Java 自带的 Unsafe 实现的 CAS。 它在虚拟机层面确保了写入数据的原子性,比加锁的效率高得多:
static final像 ConcurrentHashMap 这样的高级并发工具的确提供了一些高级 API,只有充分了解其特性才能最大化其威力,而不能因为其足够高级、酷炫盲目使用。
四、不熟悉并发工具的使用场景,因而导致性能问题除了 ConcurrentHashMap 这样通用的并发工具类之外,我们的工具包中还有些针对特殊场景实现的生面孔。 一般来说,针对通用场景的通用解决方案,在所有场景下性能都还可以,属于“万金油”;而针对特殊场景的特殊实现,会有比通用解决方案更高的性能,但一定要在它针对的场景下使用,否则可能会产生性能问题甚至是 Bug。
之前在排查一个生产性能问题时,我们发现一段简单的非数据库操作的业务逻辑,消耗了超出预期的时间,在修改数据时操作本地缓存比回写数据库慢许多。 查看代码发现,开发同学使用了 CopyOnWriteArrayList 来缓存大量的数据,而数据变化又比较频繁。
CopyOnWrite 是一个时髦的技术,不管是 Linux 还是 Redis 都会用到。 在 Java 中,CopyOnWriteArrayList 虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景。
如果我们要使用 CopyOnWriteArrayList,那一定是因为场景需要而不是因为足够酷炫。 如果读写比例均衡或者有大量写操作的话,使用 CopyOnWriteArrayList 的性能会非常糟糕。
我们写一段测试代码,来比较下使用 CopyOnWriteArrayList 和
Tomcat 或者 Jetty 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。
Spring 框架就是对 Servlet 的封装,Spring 应用本身就是一个 Servlet,而 Servlet 容器是管理和运行 Servlet 的。
Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范。 Tomcat 和 Jetty 都按照 Servlet 规范的要求实现了 Servlet 容器。
Servlet 容器工作流程:
当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端。
Servlet 规范提供了两种扩展机制:Filter和Listener。
Tomcat 要实现 2 个核心功能:
因此 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。 连接器负责对外交流,容器负责内部处理。
1,连接器
连接器需要完成 3 个高内聚的功能:
因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter。
Endpoint 和 Processor 放在一起抽象成了 ProtocolHandler 组件,连接器用 ProtocolHandler 来处理网络连接和应用层协议。
EndPoint 是一个接口,它的抽象实现类 AbstractEndpoint 里面定义了两个内部类:Acceptor 和 SocketProcessor。 其中 Acceptor 用于监听 Socket 连接请求。 SocketProcessor 用于处理接收到的 Socket 请求。
EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。
2,容器
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。 这 4 种容器不是平行关系,而是父子关系。
Context 表示一个 Web 应用程序;Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
请求定位 Servlet 的过程:Tomcat 会创建一个 Service 组件和一个 Engine 容器组件,在 Engine 容器下创建两个 Host 子容器,在每个 Host 容器下创建两个 Context 子容器。 由于一个 Web 应用通常有多个 Servlet,Tomcat 还会在每个 Context 容器里创建多个 Wrapper 子容器。
每一个容器都有一个 Pipeline 对象。
3,一个请求在 Tomcat 中流转的过程 :
4, 启动 tomcat 的过程:
粉丝福利,需获取Tomcat、spring等架构资料
内容声明:
1、本站收录的内容来源于大数据收集,版权归原网站所有!
2、本站收录的内容若侵害到您的利益,请联系我们进行删除处理!
3、本站不接受违法信息,如您发现违法内容,请联系我们进行举报处理!
4、本文地址:http://www.jujiwang.com/article/6334d38ab177f8c5eeb4.html,复制请保留版权链接!
body,font,family,Arial,Helvetica,sans,serif,h1,margin,bottom,15px,.container,width,80%,margin,0auto,.section,margin,bottom,30px,.sub,section,margin,bottom,15px,.co...。
技术教程 2024-09-30 01:53:38
在当今数据驱动的世界中,并行编程已成为应对海量计算需求至关重要的工具,OpenCL,开放式计算语言,作为一种异构并行编程语言和框架,为开发者提供了利用各种计算设备,如CPU、GPU和加速器,的强大能力,OpenCL概述OpenCL是一种跨平台语言,允许开发者使用单一编程模型针对不同的异构设备编写并行应用程序,它采用C99语言规范,并提...。
互联网资讯 2024-09-28 16:21:08
AWK是一种强大的文本处理语言,通过掌握其高级特性,可以进一步提高处理效率和扩展AWK的功能,内置函数length,string,返回字符串的长度,substr,string,start,length,从字符串中提取子字符串,split,string,array,sep,根据分隔符将字符串拆分为数组,index,string,s...。
本站公告 2024-09-25 18:12:10
简介fscanf是C标准库中用于从文本文件中读取格式化数据的灵活函数,它允许您指定一个格式字符串,其中包含各种格式说明符,如%d,整数,、%f,浮点数,和%s,字符串,fscanf将根据格式字符串,从文件中读取与之匹配的数据,在本文中,我们将分步学习如何使用fscanf从文本文件中读取学生信息,包括姓名、学号、分数等,步骤1,打开文...。
本站公告 2024-09-23 17:15:56
03cli>,Facebook,社交媒体巨头使用ReactNative构建了其移动应用程序,Instagram,照片分享应用程序使用ReactNative重新设计了其界面,Airbnb,住宿预订应用程序使用ReactNative提高了其性能和用户体验,掌握人生掌握ReactNative的高级特性可以帮助开发人员构建功能强大的移动应...。
最新资讯 2024-09-14 11:51:57
引言设计模式是解决常见软件设计问题的通用解决方案,它们通过提供经过验证的、可重用的设计原则和元素,帮助开发者编写可维护、可扩展和灵活的代码,本文将探讨PHP中常见的设计模式,包括它们的用途、优点和实现示例,了解这些模式将使你能够构建强大、可扩展的PHP应用程序,常见的设计模式1.工厂模式用途,创建一个对象,而无需指定其确切类型,优点,...。
本站公告 2024-09-12 22:05:05
在SEO和内容营销中,标题扮演着至关重要的角色,一个有吸引力的标题可以吸引读者,让他们点击你的内容,而一个平淡的标题则会让他们错过你的精彩文章,标签是提升标题吸引力的一种有效方式,通过使用相关的标签,你可以向读者展示你的内容是如何与他们的兴趣和搜索查询相关的,5个提升标题吸引力的标签技巧1.使用相关的、具体标签不要使用笼统、通用的标签...。
最新资讯 2024-09-12 07:14:59
在现代软件开发中,数据库测试自动化已成为不可或缺的一部分,它可以显著提高测试效率、准确性和代码质量,本文将深入探讨数据库测试自动化的重要性、优势和最佳实践,帮助您充分发挥其潜力,为什么要进行数据库测试自动化,数据库是许多软件应用程序的关键组件,存储着应用程序的关键数据,确保数据库的行为符合预期对于应用程序的稳定性和可靠性至关重要,手动...。
技术教程 2024-09-09 12:22:52
引言在现代软件开发中,依赖注入是一种常用的设计模式,它可以提高应用程序的模块化、可测试性和灵活性,Java接口在依赖注入中的应用尤为广泛,因为它提供了以下优势,简洁,接口只声明方法签名,不包含任何实现细节,使得代码更加简洁易读,可测试,接口可以被模拟或存根,这使得测试应用程序变得更加容易,灵活,接口允许开发者在运行时动态注入不同的实现...。
互联网资讯 2024-09-08 12:40:51
在当今以技术为中心的商业环境中,电子商务已成为必不可少的驱动力,为企业提供了扩大其市场覆盖范围,增加收入并建立忠实客户群体的巨大机会,为了充分利用电子商务的潜力,选择一个可靠且功能强大的源码解决方案至关重要,开源解决方案提供了一系列好处,例如灵活性、可定制性以及更低的运营成本,使其成为电子商务企业的理想选择,在本文中,我们将探索领先的...。
最新资讯 2024-09-07 07:24:20
前言递归函数是一种在问题求解中发挥着至关重要作用的强大工具,它们通过以较小规模的方式重复调用自身来解决复杂问题,在MATLAB中,递归函数的使用为解决各种计算问题提供了灵活且高效的途径,递归函数的本质递归函数遵循两个关键原则,1.基本案例,函数定义有一个或多个基本案例,这些案例指定问题如何针对最简单的情况进行求解,2.递归步骤,对于基...。
互联网资讯 2024-09-07 05:35:19
在当今竞争激烈的广告环境中,脱颖而出并吸引受众的注意力至关重要,对联广告策略是一种有效的技术,它利用创意和吸引力,帮助广告客户创建引人注目的、难忘的广告,对联广告策略简介对联广告策略是将两个或更多相关或互补的广告配对的做法,这些广告通常并排或上下放置,在视觉上相互补充,并传达一个连贯的信息,对联广告策略的优势提高可见度,对联广告通过在...。
互联网资讯 2024-09-06 19:45:23