不论你是否关注,Java Web应用都或多或少的使用了线程池来处理请求。线程池的实现细节可能会被忽视,但是有关于线程池的使用和调优迟早是需要了解的。本文主要介绍Java线程池的使用和如何正确的配置线程池。

单线程

我们先从基础开始。无论使用哪种应用服务器或者框架(如Tomcat、Jetty等),他们都有类似的基础实现。Web服务的基础是套接字(socket),套接字负责监听端口,等待TCP连接,并接受TCP连接。一旦TCP连接被接受,即可从新创建的TCP连接中读取和发送数据。

为了能够理解上述流程,我们不直接使用任何应用服务器,而是从零开始构建一个简单的Web服务。该服务是大部分应用服务器的缩影。一个简单的单线程Web服务大概是这样的:ServerSocket listener = new ServerSocket(8080);try { while (true) {

Socket socket = listener.accept();   try {

handleRequest(socket);

} catch (IOException e) {

e.printStackTrace();

}

}

} finally {

listener.close();

}

上述代码创建了一个 服务端套接字(ServerSocket) ,监听8080端口,然后循环检查这个套接字,查看是否有新的连接。一旦有新的连接被接受,这个套接字会被传入handleRequest方法。这个方法会将数据流解析成HTTP请求,进行响应,并写入响应数据。在这个简单的示例中,handleRequest方法仅仅实现数据流的读入,返回一个简单的响应数据。在通常实现中,该方法还会复杂的多,比如从数据库读取数据等。final static String response =

“HTTP/1.0 200 OK/r/n” +

“Content-type: text/plain/r/n” +

“/r/n” +

“Hello World/r/n”;public static void handleRequest(Socket socket) throws IOException { // Read the input stream, and return “200 OK”

try {

BufferedReader in = new BufferedReader(     new InputStreamReader(socket.getInputStream()));

log.info(in.readLine());

OutputStream out = socket.getOutputStream();   out.write(response.getBytes(StandardCharsets.UTF_8));

} finally {

socket.close();

}

}

由于只有一个线程来处理请求,每个请求都必须等待前一个请求处理完成之后才能够被响应。假设一个请求响应时间为100毫秒,那么这个服务器的每秒响应数(tps)只有10。

多线程

虽然handleRequest方法可能阻塞在IO上,但是CPU仍然可以处理更多的请求。但是在单线程情况下,这是无法做到的。因此,可以通过创建多线程的方式,来提升服务器的并行处理能力。public static class HandleRequestRunnable implements Runnable { final Socket socket; public HandleRequestRunnable(Socket socket) {   this.socket = socket;

} public void run() {   try {

handleRequest(socket);

} catch (IOException e) {

e.printStackTrace();

}

}

}

ServerSocket listener = new ServerSocket(8080);try { while (true) {

Socket socket = listener.accept();   new Thread(new HandleRequestRunnable(socket)).start();

}

} finally {

listener.close();

}

这里,accept()方法仍然在主线程中调用,但是一旦TCP连接建立之后,将会创建一个新的线程来处理新的请求,既在新的线程中执行前文中的handleRequest方法。

通过创建新的线程,主线程可以继续接受新的TCP连接,且这些信求可以并行的处理。这个方式称为“每个请求一个线程(thread per request)”。当然,还有其他方式来提高处理性能,例如 NGINX 和 Node.js 使用的异步事件驱动模型,但是它们不使用线程池,因此不在本文的讨论范围。

在每个请求一个线程实现中,创建一个线程(和后续的销毁)开销是非常昂贵的,因为JVM和操作系统都需要分配资源。另外,上面的实现还有一个问题,即创建的线程数是不可控的,这将可能导致系统资源被迅速耗尽。

资源耗尽

每个线程都需要一定的栈内存空间。在最近的64位JVM中, 默认的栈大小 是1024KB。如果服务器收到大量请求,或者handleRequest方法执行很慢,服务器可能因为创建了大量线程而崩溃。例如有1000个并行的请求,创建出来的1000个线程需要使用1GB的JVM内存作为线程栈空间。另外,每个线程代码执行过程中创建的对象,还可能会在堆上创建对象。这样的情况恶化下去,将会超出JVM堆内存,并产生大量的垃圾回收操作,最终引发 内存溢出(OutOfMemoryErrors) 。

这些线程不仅仅会消耗内存,它们还会使用其他有限的资源,例如文件句柄、数据库连接等。不可控的创建线程,还可能引发其他类型的错误和崩溃。因此,避免资源耗尽的一个重要方式,就是避免不可控的数据结构。

顺便说下,由于线程栈大小引发的内存问题,可以通过-Xss开关来调整栈大小。缩小线程栈大小之后,可以减少每个线程的开销,但是可能会引发 栈溢出(StackOverflowErrors) 。对于一般应用程序而言,默认的1024KB过于富裕,调小为256KB或者512KB可能更为合适。Java允许的最小值是160KB。

线程池

为了避免持续创建新线程,可以通过使用简单的线程池来限定线程池的上限。线程池会管理所有线程,如果线程数还没有达到上限,线程池会创建线程到上限,且尽可能复用空闲的线程。ServerSocket listener = new ServerSocket(8080);

ExecutorService executor = Executors.newFixedThreadPool(4);try { while (true) {

Socket socket = listener.accept();

executor.submit( new HandleRequestRunnable(socket) );

}

} finally {

listener.close();

}

在这个示例中,没有直接创建线程,而是使用了ExecutorService。它将需要执行的任务(需要实现Runnables接口)提交到线程池,使用线程池中的线程执行代码。示例中,使用线程数量为4的固定大小线程池来处理所有请求。这限制了处理请求的线程数量,也限制了资源的使用。

除了通过 newFixedThreadPool 方法创建固定大小线程池,Executors类还提供了 newCachedThreadPool 方法。复用线程池还是有可能导致不可控的线程数,但是它会尽可能使用之前已经创建的空闲线程。通常该类型线程池适合使用在不会被外部资源阻塞的短任务上。

工作队列

使用了固定大小线程池之后,如果所有的线程都繁忙,再新来一个请求将会发生什么呢?ThreadPoolExecutor使用一个队列来保存等待处理的请求,固定大小线程池默认使用无限制的链表。注意,这又可能引起资源耗尽问题,但只要线程处理的速度大于队列增长的速度就不会发生。然后前面示例中,每个排队的请求都会持有套接字,在一些操作系统中,这将会消耗文件句柄。由于操作系统会限制进程打开的文件句柄数,因此最好限制下工作队列的大小。public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) { return new ThreadPoolExecutor(nThreads, nThreads,     0L, TimeUnit.MILLISECONDS,     new LinkedBlockingQueue(capacity),     new ThreadPoolExecutor.DiscardPolicy());

}public static void boundedThreadPoolServerSocket() throws IOException {

ServerSocket listener = new ServerSocket(8080);

ExecutorService executor = newBoundedFixedThreadPool(4, 16); try {   while (true) {

Socket socket = listener.accept();

executor.submit( new HandleRequestRunnable(socket) );

}

} finally {

listener.close();

}

}

这里我们没有直接使用Executors.newFixedThreadPool方法来创建线程池,而是自己构建了ThreadPoolExecutor对象,并将工作队列长度限制为16个元素。

如果所有的线程都繁忙,新的任务将会填充到队列中,由于队列限制了大小为16个元素,如果超过这个限制,就需要由构造ThreadPoolExecutor对象时的最后一个参数来处理了。示例中,使用了 抛弃策略(DiscardPolicy) ,即当队列到达上限时,将抛弃新来的任务。初次之外,还有 中止策略(AbortPolicy) 和 调用者执行策略(CallerRunsPolicy) 。前者将抛出一个异常,而后者会再调用者线程中执行任务。

对于Web应用来说,最优的默认策略应该是抛弃或者中止策略,并返回一个错误给客户端(如 HTTP 503 错误)。当然也可以通过增加工作队列长度的方式,避免抛弃客户端请求,但是用户请求一般不愿意进行长时间的等待,且这样会更多的消耗服务器资源。工作队列的用途,不是无限制的响应客户端请求,而是平滑突发暴增的请求。通常情况下,工作队列应该是空的。

线程数调优

前面的示例展示了如何创建和使用线程池,但是,使用线程池的核心问题在于应该使用多少线程。首先,我们要确保达到线程上限时,不会引起资源耗尽。这里的资源包括内存(堆和栈)、打开文件句柄数量、TCP连接数、远程数据库连接数和其他有限的资源。特别的,如果线程任务是计算密集型的,CPU核心数量也是资源限制之一,一般情况下线程数量不要超过CPU核心数量。

由于线程数的选定依赖于应用程序的类型,可能需要经过大量性能测试之后,才能得出最优的结果。当然,也可以通过增加资源数的方式,来提升应用程序的性能。例如,修改JVM堆内存大小,或者修改操作系统的文件句柄上限等。然后,这些调整最终还是会触及理论上限。

利特尔法则

利特尔法则 描述了在稳定系统中,三个变量之间的关系。

其中L表示平均请求数量,λ表示请求的频率,W表示响应请求的平均时间。举例来说,如果每秒请求数为10次,每个请求处理时间为1秒,那么在任何时刻都有10个请求正在被处理。回到我们的话题,就是需要使用10个线程来进行处理。如果单个请求的处理时间翻倍,那么处理的线程数也要翻倍,变成20个。

理解了处理时间对于请求处理效率的影响之后,我们会发现,通常理论上限可能不是线程池大小的最佳值。线程池上限还需要参考任务处理时间。

假设JVM可以并行处理1000个任务,如果每个请求处理时间不超过30秒,那么在最坏情况下,每秒最多只能处理33.3个请求。然而,如果每个请求只需要500毫秒,那么应用程序每秒可以处理2000个请求。

拆分线程池

在微服务或者面向服务架构(SOA)中,通常需要访问多个后端服务。如果其中一个服务性能下降,可能会引起线程池线程耗尽,从而影响对其他服务的请求。

应对后端服务失效的有效办法是隔离每个服务所使用的线程池。在这种模式下,仍然有一个分派的线程池,将任务分派到不同的后端请求线程池中。该线程池可能因为一个缓慢的后端而没有负载,而将负担转移到了请求缓慢后端的线程池中。

另外,多线程池模式还需要避免死锁问题。如果每个线程都阻塞在等待未被处理请求的结果上时,就会发生死锁。因此,多线程池模式下,需要了解每个线程池执行的任务和它们之间的依赖,这样可以尽可能避免死锁问题。

总结

即使没有在应用程序中直接使用线程池,它们也很有可能在应用程序中被应用服务器或者框架间接使用。 Tomcat 、 JBoss 、 Undertow 、 Dropwizard 等框架,都提供了调优线程池(servlet执行使用的线程池)的选项。

希望本文能够提升对线程池的了解。通过了解应用的需求,组合最大线程数和平均响应时间,可以得出一个合适的线程池配置。

java 利特尔法则_Java Web应用中调优线程池的重要性相关推荐

  1. java web 线程数_Java Web应用调优线程池

    最简单的单线程 我们先从基础开始.无论使用哪种应用服务器或者框架(如Tomcat.Jetty等),他们都有类似的基础实现.Web服务的基础是套接字(socket),套接字负责监听端口,等待TCP连接, ...

  2. Java Web应用调优线程池

    最简单的单线程 我们先从基础开始.无论使用哪种应用服务器或者框架(如Tomcat.Jetty等),他们都有类似的基础实现.Web服务的基础是套接字(socket),套接字负责监听端口,等待TCP连接, ...

  3. 如何用利特尔法则调整线程池大小

    利特尔法则 利特尔法则派生于排队论,用以下数学公式表示: L=λWL = λW L=λW L 系统中存在的平均请求数量. λ 请求有效到达速率.例如:5/s 表示每秒有5个请求到达系统. W 请求在系 ...

  4. 一起学JAVA之【基础篇】4种默认线程池介绍

    一起学JAVA之[基础篇]4种默认线程池介绍 默认线程池创建方式 java.util.concurrent 提供了一个创建线程池的工具类Executors,里面有四种常用的线程池创建方法 public ...

  5. java线程池拒绝策略_Java核心知识 多线程并发 线程池原理(二十三)

    线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后 启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕, 再从队列中取出任务来执行.他 ...

  6. java 禁止使用多线程_Java多线程(四)-线程状态的转换 - Java 技术驿站-Java 技术驿站...

    一.线程状态 线程的状态转换是线程控制的基础.线程状态总的可分为五大状态:分别是生.死.可运行.运行.等待/阻塞.用一个图来描述如下: 1.新状态:线程对象已经创建,还没有在其上调用start()方法 ...

  7. java构造单例线程池_java中常见的六种线程池详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如 ...

  8. Java基础巩固(二)异常,多线程,线程池,IO流,Properties集合,IO工具类,字符流,对象流,Stream,Lambda表达式

    一.异常,多线程 学习目标 : 异常的概述 异常的分类 异常的处理方式 自定义异常 多线程入门 1 异常的概述 1.1 什么是异常? 异常就是程序出现了不正常情况 , 程序在执行过程中 , 数据导致程 ...

  9. 线程池参数详解_java中常见的六种线程池详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如 ...

最新文章

  1. 从创建数据库到备份恢复还原详解
  2. HDFS设置配额管理
  3. Ajax调用MVC控制器参数为实体
  4. 欢乐纪中某B组赛【2019.1.24】
  5. gitlab访问慢,出现502,特别卡,耗内存cpu解决办法
  6. pecl安装扩展(首选)
  7. 爹地,我找到了!15个极好的Linux find命令示例
  8. O(logn)复杂度恐怖之处
  9. php学生选课系统设计网站作品
  10. 用C#编写验证码的方法
  11. 基本概念:线与逻辑、锁存器、缓冲器、建立时间
  12. Spring框架 教程
  13. OSEK-NM直接网络管理一:概念部分
  14. 谷歌浏览器安装stylish插件笔记
  15. 第一章 空间解析几何与向量代数(1)
  16. 换个角度看前几天的女孩父亲杀男孩事件 另附一些对当前教育的感想
  17. “2.17亿中国电信”拿下国家税务局云平台项目,H3C却是最大赢家
  18. The valid characters are defined in RFC 7230 and RFC 3986
  19. 数据的相似性和相异性的度量
  20. 最新研究表明:熬夜会增加患癌症几率

热门文章

  1. 华为云IoT智慧物流案例01 | 背景介绍与环境搭建
  2. Ubuntu 安装 Sublime text 解决搜狗输入法问题
  3. 解析世界杯超大规模直播场景下的码率控制
  4. 【Realtek sdk-4.4.1】RTL8198D升级uboot和固件操作方法
  5. 【转】 快捷方式lnk文件格式详解(英文)(中文)
  6. error: 'ff_get_buffer' was not declared in this scope
  7. Java大数据-反射和动态代理
  8. java 多线程。 编写10个线程,第一个线程从1加到10,第二个线程第11加到20,。。。第10个线程从91加到100.最够把10个线程结果相加
  9. 母亲骑摩托4千里回家看儿子 为保险女扮男装
  10. Final IK(3)——VRIK使用方法和一些小技巧