Web容器线程池机制小议

基础

从刚开始学习java,我们就被告知Java是一种支持多线程的语言,每条程序指令都会在一个线程中执行,而启动主线程的入口,是可执行类中的main方法。我们可以在main方法或其调用的方法中创建新的线程以实现多线程、并发处理的效果。

Java入门资料上介绍线程时往往会说明一点,创建线程不是免费的,是有成本的--对内存的消耗、对CPU切换调度的消耗都是成本,所以像数据库连接池这类“创建昂贵型”资源一样,创建好的线程优先被复用而不是每次都创建新的,这就是线程池出现的原因。

用户请求进入JVM的途径

我们可以把启动后的JVM看成一个一间屋子,用户请求要进入这个屋子,上面提到的main方法是一种-启动的时候即传入请求参数。另外一种最常用的方式,就是Socket。JavaIO体系中的socket类为这个屋子打开了一扇门,使得各种在socket之上的协议请求可以自由地进入到JVM中,其中最大的一种应用,是servlet编程模型。各类基于request-reply的同步请求,不论是基本的socket请求,还是http、webservice、hessian请求,都是通过最基本的socket抽象,进入到JVM的屋子并进而被JVM内的线程处理。-这是Web容器实现的基本原理。

我们常用的另一种请求进入JVM的途径是通过JMS消息,追到底层实现仍然逃不出socket抽象,只不过这种方式可以实现除request-reply外更多的消息交换模式(MEP)而已。

Web容器机理

有了上面的铺垫,我们不难想象web容器(tomcat,jetty,weblogic等)的实现了:通过一个主入口类提供的main方法,创建一个服务端socket管理类监听指定的socket,创建一个线程池处理接收到的请求。典型场景是:来自客户端的socket请求到达服务端时,服务端socket管理类提取这个socket实例,然后将它交给线程池中一个空闲线程处理,处理完毕后,线程回到连接池,结果写回客户端,socket被关闭。

常见的web容器(tomcat,jetty,weblogic等)都是基于这种socket+连接池的模型来实现的,以tomcat为例,其server.xml文件中配置的Connector标签是对不同socket之上的协议的抽象,如处理http的connector,处理https的connector,处理ajp的connector等。Executor标签是对连接池的抽象,可以配置最大线程数,初始线程数等。可以用如下方式查看windows下运行态的tomcat线程:

在tomcat运行窗口中,按下快捷键ctrl+break。之后JVM内的线程信息就会输出到tomcat运行窗口中,在其中我们可以看到类似“"TP-Processor4"daemonprio=6tid=0x0325ae30nid=0x10a0runnable”这样的线程,就是线程池中等待处理用户请求的线程,还有类似“"http-8080-Acceptor-0"daemonprio=6tid=0x0327b588nid=0x6a0runnable”这样的线程,可以推断是接收客户端socket请求的线程。

拓展话题

到这里,我们讲清楚了web容器工作的基本原理,这个可以解释为什么tomcat出现threadpoolfull的错误---线程池中所有的线程都忙着呢,当然它们都在忙啥,还需要进一步挖掘。

值得注意的问题一:Quartz框架内置了自己的线程池,应该不是使用服务器的线程工作的(这个我还没有通过试验证明,依稀记得配置了Quartz的应用启动后tomcat的threaddump会不同)。

值得注意的问题二:因为我们的应用程序是在web容器的线程上下文中执行,所以一些线程相关的操作会直接影响到服务器的线程池(例如建超的测试中就是把所有web容器线程池中的线程给催眠了),一个容易犯编程错误的场景是ThreadLocal的使用。ThreadLocal的生命周期是在一个线程内,在线程池内的线程生命周期被延长了,一个线程在其生命周期内可能处理无数个用户请求,所以不要假设你放在ThreadLocal内的变量只会被单个请求处理中被访问。

值得注意的问题三:上面提到的一个socket请求一个线程来处理的方式是传统的web容器实现方式,JavaNIO带来了异步IO处理之后,Web容器以及JEE规范也在发生着变化。Web容器的变化是对NIO的支持,使用NIO,可以让少数的线程处理更多的来自客户的并发请求而不再像上面那样线程池耗尽后无法处理用户请求,以tomcat为例,6.x版本之后提供了对NIO的支持,可以通过配置Http11NioProtocol取代原来的Http11Protocol来实现。JEE规范的变化是Servlet3中对异步请求处理的支持,可以实现服务器推等目前编程模型中无法想象的效果。

关于线程安全

1、线程安全的第一重意义,是指Java中的某些基本类型的操作是非原子性的,比如long和double这类64位变量,当线程把主存中的long或者double型的数据读入线程内存时,可能是两次读操作,写入也是,这种非原子性的类型操作在多线程环境下会出现问题--一个简单的doubled=1.0d指令的执行都可能会出错!

2、线程安全的第二重意义,是我们常说的这种多线程对宏观业务逻辑的影响,多个线程中的代码读写主存中的变量值,使得单个线程中的处理逻辑发生紊乱乃至错误。

ThreadLocal是解决多线程问题的一种,其他的还有synchronized关键字、volatile关键字等。

虽然说开发服务端程序不用太多的考虑多线程问题,但是不代表不需要考虑,下面提到的点就是web应用开发中需要注意多线程问题的地方:

1、单例。Spring管理的controller、service,structs1中的action,这些对象都是单例的,可写的实例变量存在多线程问题。

2、共享资源。比如数据库连接,文件句柄等,这些显然不能被多线程共享使用。

3、框架的线程安全问题。比如hibernate文档就声明了其session类不是线程安全的。

4、Java基础框架中的线程问题。比如Servlet规范明确说明了对HttpSession类中attribute的处理是需要开发者来保证线程安全的;早期的EJB规范中也明确禁止在Bean中进行多线程编程。

相关推荐