高性能的本质

高性能程序本质就是充分利用CPU、内存等资源,在短时间处理大量请求。
所谓的充分利用,一方面是指避免线程阻塞,阻塞就会浪费CPU资源。另一方面是指可以权衡利弊,牺牲一种资源弥补另一种资源。

比如缓存和对象池技术就是用内存换 CPU;
数据压缩后再传输就是用 CPU 换网络。

最大并发连接数

maxConnection + acceptCount

Tomcat/Jetty 的设计

为了高性能,Tomcat/Jetty的设计有以下几点: I/O 和线程模型、减少系统调用、池化、零拷贝、高效的并发编程。

池化的本质就是用内存换 CPU;零拷贝就是不做无用功,减少资源浪费。

如何看待I/O模型和线程模型的关系?

I/O模型

I/O 模型的本质就是解决 CPU 和外设之间的速度差,目标就是尽量减少数据复制时的线程阻塞。

Tomcat 和 Jetty 采用了非阻塞 I/O 或者异步 I/O,目的是业务线程不需要阻塞在 I/O 等待上。

线程模型

Tomcat 和 Jetty 的总体处理原则是:
连接请求由专门的 Acceptor 线程组处理。
I/O 事件侦测也由专门的 Selector 线程组来处理。
具体的协议解析和业务处理可能交给线程池(Tomcat),或者交给 Selector 线程来处理(Jetty)。采用异步 IO 模型,就不需要 Selector 组件侦测 IO 事件。

将这三个步骤分开的好处是解耦,可以根据实际情况合理设置各部分的线程数。但是线程数并不是越多越好,因为 CPU 核的个数有限,线程太多也处理不过来,会导致大量的线程上下文切换。

减少系统调用

系统调用涉及 CPU 从用户态切换到内核态的过程,非常耗资源。因此要有意识尽量避免系统调用。

具体有两种设计思路,###基于缓冲区和延迟解析。

基于缓冲区可以减少系统调用的次数。这也是缓冲的原始含义!
因为每一次系统调用都很浪费资源,因此可以先放到缓冲区,累积之后再写。

比如在 Tomcat 和 Jetty 中,系统调用最多的就是网络通信操作。一个 Channel 上的 write 就是系统调用,为了降低系统调用的次数,最直接的方法就是使用缓冲,当输出数据达到一定的大小才 flush 缓冲区。Tomcat 和 Jetty 的 Channel 都带有输入输出缓冲区。

Tomcat 和 Jetty 在解析 HTTP 协议数据时,都采取了延迟解析的策略。
HTTP 的请求体(HTTP Body)直到用的时候才解析。当 Tomcat 调用 Servlet 的 service 方法时,只是读取了和解析了 HTTP 请求头,并没有读取 HTTP 请求体。

直到 Web 应用程序调用了 ServletRequest 对象的 getInputStream 方法或者 getParameter 方法时,Tomcat 才会去读取和解析 HTTP Body;没有调用上面那两个方法,Body 不会被读取和解析,这样就省掉了一次 I/O 系统调用。

高效的并发编程

并发的过程中,为了同步多个线程对共享变量的访问,需要加锁来实现。而锁的开销是比较大的,拿锁的过程本身就是个系统调用,如果锁没拿到线程会阻塞,又会发生线程上下文切换,尤其是大量线程同时竞争一把锁时,会浪费大量的系统资源。

因此要有意识的尽量避免锁的使用,比如可以使用原子类 CAS 或者并发集合来代替。如果万不得已需要用到锁,也要尽量缩小锁的范围和锁的强度。

Tomcat 和 Jetty 如何做到高效的并发编程?

用原子变量和 CAS 取代锁
Jetty 线程池的启动方法:启动一个线程时,首先通过 get 方法拿到值,如果线程数已经达到最大值,直接返回。否则尝试用 ###CAS 操作将如果成功了意味着没有其他线程在改这个值,当前线程可以继续往下执行;否则走 continue 分支,也就是继续重试,直到成功为止。

并发容器的使用
CopyOnWriteArrayList 适用于读多写少的场景。比如 Tomcat 用它来“存放”事件监听器,这是因为监听器一般在初始化过程中确定后就基本不会改变,当事件触发时需要遍历这个监听器列表,所以这个场景符合读多写少的特征。

Volatile 关键字
以 Tomcat 中的 LifecycleBase 作为例子,里面的生命状态就是用 volatile 关键字修饰的。volatile 的目的是为了保证一个线程修改了变量,另一个线程能够读到这种变化。对于生命状态来说,需要在各个线程中保持是最新的值,因此采用了 volatile 修饰。

volatile 保证生命状态对所有线程都是一样的。

知识

JDK1.6 以后 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。