## 问题一 @小美 既然一个 JVM 是一个进程,JVM 上跑 Tomcat,Tomcat 上可以部署多个应用。这样的话,每个跑在 Tomcat 上的应用是一个线程吗?该怎么理解“如果一个应用 crash 了,其他应用也会 crash”? 理解程序运行时的执行环境,直观感受程序是如何运行的,对我们开发和维护软件很有意义。我们以小美同学提的这个场景为例,看下 Java Web 程序的运行时环境是什么样的,来重新梳理下进程、线程、应用、Web 容器、Java 虚拟机和操作系统之间的关系。 我们用 Java 开发 Web 应用,开发完成,编译打包以后得到的是一个 war 包,这个 war 包放入 Tomcat 的应用程序路径下,启动 Tomcat 就可以通过 HTTP 请求访问这个 Web 应用了。 在这个场景下,进程是哪个?线程有哪些?Web 程序的 war 包是如何启动的?HTTP 请求如何被处理?Tomcat 在这里扮演的是什么角色?JVM 又扮演什么角色? 首先,我们是通过执行 Tomcat 的 Shell 脚本启动 Tomcat 的,而在 Shell 脚本里,其实启动的是 Java 虚拟机,大概是这样一个 Shell 命令: java org.apache.catalina.startup.Bootstrap "$@" start 所以我们在 Linux 操作系统执行 Tomcat 的 Shell 启动脚本,Tomcat 启动以后,其实在操作系统里看到的是一个 JVM 虚拟机进程。这个虚拟机进程启动以后,加载 class 进来执行,首先加载的就这个org.apache.catalina.startup.Bootstrap类,这个类里面有一个main()函数,是整个 Tomcat 的入口函数,JVM 虚拟机会启动一个主线程从这个入口函数开始执行。 主线程从 Bootstrap 的 main() 函数开始执行,初始化 Tomcat 的运行环境,这时候就需要创建一些线程,比如负责监听 80 端口的线程,处理客户端连接请求的线程,以及执行用户请求的线程。创建这些线程的代码是 Tomcat 代码的一部分。 初始化运行环境之后,Tomcat 就会扫描 Web 程序路径,扫描到开发的 war 包后,再加载 war 包里的类到 JVM。因为 Web 应用是被 Tomcat 加载运行的,所以我们也称 Tomcat 为 Web 容器。 如果有外部请求发送到 Tomcat,也就是外部程序通过 80 端口和 Tomcat 进行 HTTP 通信的时候,Tomcat 会根据 war 包中的 web.xml 配置,决定这个请求 URL 应该由哪个 Servlet 处理,然后 Tomcat 就会分配一个线程去处理这个请求,实际上,就是这个线程执行相应的 Servlet 代码。 我们回到小美同学的问题,Tomcat 启动的时候,启动的是 JVM 进程,这个进程首先是执行 JVM 的代码,而 JVM 会加载 Tomcat 的 class 执行,并分配一个主线程,这个主线程会从 main 函数开始执行。在主线程执行过程中,Tomcat 的代码还会启动其他一些线程,包括处理 HTTP 请求的线程。 而我们开发的应用是一些 class,被 Tomcat 加载到这个 JVM 里执行,所以,即使这里有多个应用被加载,也只是加载了一些 class,我们的应用被加载进来以后,并没有增加 JVM 进程中的线程数,也就是 web 应用本身和线程是没有关系的。 而 Tomcat 会根据 HTTP 请求 URL 执行应用中的代码,这个时候,可以理解成每个请求分配一个线程,每个线程执行的都是我们开发的 Web 代码。如果 Web 代码中包含了创建新线程的代码,Tomcat 的线程在执行代码时,就会创建出新的线程,这些线程也会被操作系统调度执行。 如果 Tomcat 的线程在执行代码时,代码抛出未处理的异常,那么当前线程就会结束执行,这时控制台看到的异常信息,其实就是线程堆栈信息,线程会把异常信息以及当前堆栈的方法都打印出来。事实上,这个异常最后还是会被 Tomcat 捕获,然后 Tomcat 会给客户端返回一个 500 错误。单个线程的异常不影响其他线程执行,也就是不影响其他请求的处理。 但是如果线程在执行代码的时候,抛出的是 JVM 错误,比如OutOfMemoryError,这个时候看起来是应用 crash,事实上是整个进程都无法继续执行了,也就是进程 crash 了,进程内所有应用都不会被继续执行了。 从 JVM 的角度看,Tomcat 和我们的 Web 应用是一样的,都是一些 Java 代码,但是 Tomcat 却可以加载执行 Web 代码,而我们的代码又不依赖 Tomcat,这也是一个很有意思的话题。Tomcat 是如何设计的,我将会在下个模块讲述。 ## 问题二 @黄海峰 有点难以想象,“Hash 表的时间复杂度为什么是 O(1)”这个问题居然有阿里大厂的面试官觉得难。 这不是一个疑问,但其实是一个有意思的话题,我们花一点时间讨论下,也许会对你的职业规划有所启发。 文中这个故事大概发生在 2009 年,整整十年前,那个时候互联网还不像今天这样炙手可热,提供的薪水也不像今天这样有竞争力,也没有 BAT 这样的专有名词指代所谓的互联网巨头。那个时候,计算机专业优秀的毕业生向往的是微软、Oracle、IBM 这样的外资 IT 巨头,退而求其次,国内好的 IT 公司是联想、用友这些企业。 事实上,那个时候在技术研发能力上,互联网公司的技术能力也是落后传统企业的,阿里巴巴最核心的数据存储依赖的是 IBM、Oracle、EMC 的解决方案,即所谓的 IOE。 所以在十年前的人才市场上,国内互联网公司的形象一般是:技术落后、薪水一般、加班严重、没有名气。可以说在人才市场的竞争中,相比国内外的 IT 巨头是落于下风的。 我个人感觉,互联网公司的崛起大概是在七八年前,移动互联网开始出现,互联网的渗透率得到加速,BAT 逐渐开始成为家喻户晓的名字,名气大涨。其次,经过前面时间的积累,互联网企业主导的各种分布式技术、大数据技术、移动互联网技术、云计算技术的风头超过传统 IT 巨头,阿里巴巴开始去 IOE,打造自己的云计算平台,成为先进技术的代表者;最主要的还是互联网企业盈利能力大幅增加,能够提供市场上更有竞争力的薪水和股票。 于是互联网企业在人才市场上开始变得灼手可热,BAT 这些企业开始被人称为“大厂”。我们今天感觉这些互联网巨头高高在上,人们纷纷向往。事实上,这个现象出现的时间非常短。今天这些企业有足够的名气和资源将自己营造得高高在上,可以在众多优秀的候选人中间挑来选去,仅仅在十年前,还不是这样的。 但是事情真正的吊诡之处还不在这里,当今这些互联网大厂的核心技术和业务模式在十几年前就已经奠定了,经过几年的摸索,大概在七八年前开始稳定成熟。也就是说,互联网企业的技术实力和商业能力是在这些企业还默默无闻的时候就发展起来的,而在这些企业成为明星之后,并没有什么突破性的进展。想想这些所谓的互联网大厂,最近几年,并没有什么值得称道的商业模式创新和技术创新。 也就是说,十多年前,可能是一些并不优秀的技术人员加入一个并不出名的公司,然后这些人开创出了一个杰出的事业。用马云的话说,就是“二流的人做一流的事”。然后公司开始挑选一流的人,但结果似乎只是在维持这个事业,并没有开创出更加杰出的事业。今天的 BAT 似乎成为当年的 IBM,历史好像进入了某种循环。 如果这就是事情的真相,我想你或许可以从其中得到某些启发,重新考虑下未来的职业规划。也许你会发现,你可能不需要追逐当前所谓的热门技术,而应该好好想想需要为自己的未来准备些什么。 最后,在第一模块中,我在每一篇文章的下面都留了几道思考题,各位同学在评论区都有很好的答案。但只有第五篇文章,我似乎没有看到比较准确的答案,我在这里回答一下。 RAID5 中,校验位之所以螺旋式地落在所有硬盘上,主要原因是因为如果将校验位记录在同一块硬盘上,那么对于其他多块数据盘,任何一块硬盘修改数据,都需要修改这个校验盘上的校验数据,也就是说,对于有 8 块硬盘的 RAID5 阵列,校验盘的数据写入压力是其他数据盘的 7 倍。而硬盘的频繁写入会导致硬盘寿命缩短,校验盘会频繁损坏,存储的整体可用性和维护性都会变差。 所以,作为软件架构师,当你在进行软件设计的时候,你不光需要考虑软件本身,你还需要了解软件的各种约束,硬盘的特性约束是一种,当然还有其他一些约束,我会在专栏的后面模块中继续讲解如何在各种约束下,设计出符合期望的软件系统。