调整线程池的重要性

无论您是否知道,您的Java Web应用程序很可能都使用线程池来处理传入的请求。 这是许多人忽略的实现细节,但是迟早您需要了解如何使用该池以及如何为您的应用程序正确调整池。 本文旨在说明线程模型,什么是线程池以及正确配置线程池所需执行的操作。

单螺纹

让我们从一些基础知识开始,并随着线程模型的发展而前进。 无论您使用哪种应用程序服务器或框架, Tomcat , Dropwizard , Jetty ,它们都使用相同的基本方法。 一个深埋在Web服务器内部的套接字。 该套接字正在侦听传入的TCP连接,并接受它们。 一旦被接受,就可以从新建立的TCP连接中读取数据,进行解析并将其转换为HTTP请求。 然后将此请求移交给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();
}

此代码在端口8080上创建一个ServerSocket ,然后在一个紧密循环中,ServerSocket检查是否接受新连接。 一旦接受,套接字将传递给handleRequest方法。 该方法通常将读取HTTP请求,执行所需的任何过程,然后编写响应。 在此简单示例中,handleRequest读取一行,并返回简短的HTTP响应。 handleRequest做一些更复杂的事情是正常的,例如从数据库中读取或进行某种其他类型的IO。

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();}
}

由于只有一个线程处理所有接受的套接字,因此在接受下一个请求之前,必须完全处理每个请求。 在实际的应用程序中,等效的handleRequest方法返回大约100毫秒的时间可能是正常的。 如果是这种情况,服务器将被限制为每秒仅处理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连接并且有可用的套接字,就会产生一个新线程。 这个产生的线程执行一个HandleRequestRunnable,它从上面简单地调用相同的handleRequest方法。

创建新线程后,现在可以释放原始的accept()线程来处理更多的TCP连接,并允许应用程序同时处理请求。 该技术被称为“每个请求线程”,是最流行的方法。 值得注意的是,还有其他方法,例如事件驱动的异步模型NGINX和Node.js部署,但是它们不使用线程池,因此不在本文讨论范围之内。

在“每个请求的线程”方法中,创建新线程(然后销毁它)可能会很昂贵,因为JVM和OS都需要分配资源。 另外,在上述实现中,正在创建的线程数不受限制。 不受限制是很成问题的,因为它会很快导致资源枯竭。

资源枯竭

每个线程都需要一定数量的内存用于堆栈。 在最新的64位JVM上, 默认堆栈大小为1024KB。 如果服务器收到大量请求,或者handleRequest方法变慢,则服务器可能会出现大量并发线程。 因此,要管理1000个并发请求,仅用于线程堆栈的1000个线程将消耗1GB的JVM RAM。 另外,在每个线程中执行的代码将在处理请求所需的堆上创建对象。 这非常Swift地加起来,并且可能超过分配给JVM的堆空间,从而对垃圾收集器施加压力,从而导致崩溃并最终导致OutOfMemoryErrors 。

线程不仅消耗RAM,而且可能使用其他有限资源,例如文件句柄或数据库连接。 超过这些可能导致其他类型的错误或崩溃。 因此,为了避免耗尽资源,重要的是避免无限制的数据结构。

不是灵丹妙药,但是可以通过使用-Xss标志调整堆栈大小来缓解堆栈大小问题。 较小的堆栈将减少每个线程的开销,但可能导致StackOverflowErrors 。 您的里程会有所不同,但是对于许多应用程序,默认的1024KB过多,因此较小的256KB或512KB值可能更合适。 Java允许的最小值是16KB。

线程池

为了避免连续创建新线程并限制最大数量,可以使用简单的线程池。 简而言之,该池跟踪所有线程,在需要达到上限时创建新线程,并在可能的情况下重用空闲线程。

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术语)。 在此示例中,四个线程的固定线程池用于处理所有传入的请求。 这限制了“进行中”请求的数量,因此限制了资源的使用。

除了newFixedThreadPool之外 ,Executors实用程序类还提供了newCachedThreadPool方法。 这受到较早的无限线程数量的困扰,但是只要有可能,就利用先前创建但现在空闲的线程。 通常,这种类型的池对于不会阻塞外部资源的短暂请求很有用。

ThreadPoolExecutors可以直接构造,从而可以自定义其行为。 例如,可以定义池中线程的最小和最大数量,以及何时创建和销毁线程的策略。 简短的示例。

工作队列

在固定线程池的情况下,细心的读者可能想知道如果所有线程都忙,并且有新请求进入,会发生什么情况。那么ThreadPoolExecutor使用队列来容纳线程可用之前的待处理请求。 默认情况下,Executors.newFixedThreadPool和Executors.newCachedThreadPool都使用无界LinkedList。 再次,这会导致资源耗尽问题,尽管速度要慢得多,因为每个排队的请求都小于完整线程,并且通常不会使用那么多资源。 但是,在我们的示例中,每个排队的请求都持有一个套接字(取决于OS)将占用一个文件句柄。 这是操作系统将限制的资源类型,因此除非有必要,否则最好不要保留它。 因此,限制工作队列的大小也很有意义。

public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(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个元素的有界LinkedBlockingQueue 。 或者,可以使用ArrayBlockingQueue ,它是有界缓冲区的实现。

如果所有线程都忙,并且队列已满,则下一步将由ThreadPoolExecutor的最后一个参数定义。 在此示例中,使用了DiscardPolicy ,它仅丢弃将使队列溢出的所有工作。 还有其他政策,如AbortPolicy它抛出一个异常,或CallerRunsPolicy执行该调用者的线程上的工作。 该CallerRunsPolicy提供了一种简单的方法来自我限制可以添加作业的速率,但是,这可能是有害的,阻塞了一个应保持畅通的线程。

一个好的默认策略是“放弃”或“中止”,这两者都会放弃工作。 在这些情况下,很容易向客户端返回一个简单的错误,例如HTTP 503“服务不可用” 。 有人会争辩说只是增加队列大小,然后所有工作最终都会运行。 但是,用户不愿永远等待,如果从根本上说工作的执行速度超过了可以执行的速度,那么队列将无限期地增长。 相反,该队列仅应用于消除突发请求,或处理处理中的短暂停顿。 在正常操作中,队列应为空。

有多少个线程?

现在我们了解了如何创建线程池,困难的问题是应该有多少个线程可用? 我们确定最大数量应该限制为不导致资源耗尽。 这包括所有类型的资源,内存(堆栈和堆),打开的文件句柄,打开的TCP连接,远程数据库可以处理的连接数以及任何其他有限资源。 相反,如果线程是CPU绑定的,而不是IO绑定的,则应将物理核心的数量视为有限,并且每个核心最多只能创建一个线程。

这一切都取决于应用程序正在做的工作。 用户应使用各种池大小和实际的请求混合来运行负载测试。 每次增加它们的线程池大小直到断点。 这样就可以找到资源耗尽时的上限。 在某些情况下,明智的做法是增加可用资源的数量,例如为JVM提供更多的RAM,或调整OS以允许更多的文件句柄。 但是,在某个时候将达到理论上限,应该注意,但这还不是故事的结局。

利特尔定律

Littlelaw

排队论,尤其是利特尔定律 ,可以用来帮助理解线程池的属性。 简单来说,利特尔定律描述了三个变量之间的关系。 L进行中的请求数量,λ新请求到达的速率,W平均处理请求的时间。 例如,如果每秒有10个请求到达,并且每个请求花费一秒钟的时间来处理,则在任何时间平均有10个正在进行的请求。 在我们的示例中,这映射为使用10个线程。 如果处理单个请求的时间增加了一倍,则运行中的平均请求数也将增加一倍,达到20,因此需要20个线程。

了解执行时间对进行中的请求的影响非常重要。 某些后端资源(例如数据库)停顿是很常见的,导致请求花费更长的时间来处理,从而很快耗尽了线程池。 因此,理论上限可能不是池大小的适当限制。 相反,应该对执行时间设置一个限制,并与理论上限结合使用。

例如,假设在JVM超过其内存分配之前,可以处理的最大传输中请求为1000。 如果我们预算每个请求的时间不超过30秒,那么在最坏的情况下,我们应该期望每秒处理不超过33个请求。 但是,如果一切正常,并且请求仅用500毫秒即可处理,则应用程序每秒只能在1000个线程上处理2000个请求。 指定可以使用队列来消除短暂的延迟突发也可能是合理的。

为什么要麻烦?

如果线程池中的线程太少,则存在以下风险:资源利用不足,不必要地将用户拒之门外。 但是,如果允许太多线程,则会发生资源耗尽,这可能会造成更大的破坏。

不仅会耗尽本地资源,还可能对其他资源产生不利影响。 例如,多个应用程序查询同一个后端数据库。 数据库通常对并发连接数有硬性限制。 如果一个行为异常的应用程序消耗了所有这些连接,它将阻止其他应用程序访问数据库。 造成大范围的中断。

更糟糕的是,可能会发生级联故障。 想象一下一个环境,其中有一个应用程序的多个实例,位于一个公共负载均衡器的后面。 如果实例之一由于正在进行的请求过多而开始用尽内存,那么JVM将花费更多时间进行垃圾收集,并减少处理请求的时间。 这种减慢速度将降低该实例的容量,并迫使其他实例处理更高比例的传入请求。 随着他们现在使用其无限制线程池处理更多请求,会发生相同的问题。 它们耗尽了内存,然后再次开始积极地进行垃圾回收。 这个恶性循环在所有实例之间级联,直到出现系统性故障。

我经常观察到没有进行负载测试,并且允许任意数量的线程。 在通常情况下,应用程序可以使用少量线程以传入速率愉快地处理请求。 但是,如果处理请求取决于远程服务,并且该服务暂时变慢,则W的增加(平均处理时间)的影响会很快耗尽池。 由于从未对应用程序进行最大数量的负载测试,因此会出现之前概述的所有资源耗尽问题。

有多少个线程池?

在微 服务或面向服务的体系结构 (SOA)中,访问多个远程后端服务是正常的。 此设置特别容易发生故障,因此应仔细解决这些问题。 如果远程服务的性能下降,则可能导致线程池快速达到其极限,从而丢弃后续请求。 但是,并非所有请求都可能需要此不正常的后端,但是由于线程池已满,因此不必要地删除了这些请求。

通过提供特定于后端的线程池,可以隔离每个后端的故障。 在这种模式下,仍然只有一个请求工作程序池,但是如果请求需要调用远程服务,则工作将转移到该后端的线程池。 这使主请求池不会受到单个缓慢后端的负担。 这样,只有需要特定后端池的请求才会在故障时受到影响。

多个线程池的最后一个好处是,它有助于避免某种形式的死锁。 如果由于尚未处理的请求而导致每个可用线程都被阻塞,则将发生死锁,并且没有线程可以前进。 当使用多个池并充分了解它们执行的工作时,可以在某种程度上缓解此问题。

截止日期和其他最佳做法

常见的最佳做法是确保所有远程呼叫都有最后期限。 也就是说,如果远程服务在合理时间内没有响应,则该请求将被放弃。 可以在线程池中使用相同的技术。 具体来说,如果线程正在处理一个请求的时间超过了定义的期限,则应终止该线程。 为新请求腾出空间,并在W上设置上限。这似乎是一种浪费,但是如果用户(通常是Web浏览器)正在等待响应,则30秒后,浏览器可能只会给出无论如何,还是用户可能会变得急躁并离开。

快速失败是在为后端创建池时可以采用的另一种方法。 如果后端发生故障,则线程池将Swift填充等待连接到无响应后端的请求。 相反,可以将后端标记为不正常,所有后续请求都可能立即失败,而不是不必要地等待。 但是请注意,需要一种机制来确定后端何时再次恢复健康。

最后,如果一个请求需要独立地调用多个后端,则应该可以并行而不是顺序地调用它们。 这将减少等待时间,但以增加线程为代价。

幸运的是,有一个很棒的库hystrix ,它打包了许多这些最佳实践,并以简单安全的方式公开了它们。

结论

希望本文能增进您对线程池的了解。 通过了解应用程序的需求,并结合使用最大线程数和平均响应时间,可以确定适当的线程池。 这不仅可以避免级联故障,而且可以帮助计划和配置您的服务。

即使您的应用程序可能未显式使用线程池,但它们还是被应用程序服务器或更高级别的抽象隐式使用。 Tomcat , JBoss , Undertow , Dropwizard都为其线程池(执行servlet的池)提供了多个可调参数。

翻译自: https://www.javacodegeeks.com/2015/12/importance-tuning-thread-pools.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/356073.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在线原理图绘制网站推荐

如今专业EDA软件已经基本在硬件公司普及并正版化&#xff0c;优秀的EDA工具包括 Cadence公司的OrCAD、Allegro软件&#xff0c;Mentor Graphics的PADS&#xff0c; Altium公司的Altium Designer等等&#xff0c;但是它们在功能异常强大的同时也在一些时候显得非常复杂&#xff…

java map与set的区别_java 集合(list,set,map)三者之间的关系和区别

原java 集合(list&#xff0c;set&#xff0c;map)三者之间的关系和区别一&#xff1a;先上一张关系图&#xff0c;让大家看的更明白。备注&#xff1a;其中红色部分为实现&#xff0c;其他地方均为接口。二&#xff1a;各自的特点。List 有序,可重复ArrayList优点: 底层数据结…

adf时间作用域_ADF任务流:页面片段的托管bean作用域

adf时间作用域介绍 当我们使用ADF任务流并需要实现一些特定于流的业务逻辑或存储一些与流相关的信息时&#xff0c;我们通常使用pageFlowScope托管bean。 而且&#xff0c;当我们需要为流的活动&#xff08;页面或页面片段&#xff09;提供服务时&#xff0c;我们会为此类托管b…

IMA文件如何打开,winimage使用方

一般先用UltraISO打开一个系统的镜像文件&#xff08;.iso&#xff09;。其中有些文件&#xff08;尤其是.ima,img&#xff09;比如下面雨林木风Ghost系统盘的这个IMA文件&#xff0c;我们先提取到桌面 用WinImage打开这个文件即可发现这个IMA文件整合了很多东西。所以&#x…

Java的几何布朗运动

Wiener过程是连续时间随机过程&#xff0c;以纪念Norbert Wiener命名。 通常用于用随机成分表示噪音或财务状况。 可以计算几何布朗运动以可视化某些界限&#xff08;以分位数表示&#xff09;以暗示绝对范围。 为了进行计算&#xff0c;需要以下参数&#xff1a; &#xff0…

mongodb java id 查询数据_java 用 _id 查找 MongoDB 下的数据

找网上的资料看了下增删改查&#xff0c;等日后补上。已经实现了数据的插入&#xff0c;现在想通过 _id属性来查找数据。一开始看到 类似 55b321df715cc162076eb466 这么一长串的内容觉得是string类型。但是发现并不能搜索到结果&#xff0c;在网上搜到了解决方案&#xff1a;S…

java maven部署_eclipse中maven项目部署到tomcat

下面就一一介绍这几种部署方式&#xff1a;1.打war包到tomcat/webapps目录这种方式其实跟非maven项目没什么区别&#xff0c;就是打包的方式不同之后在target目录下会生成war包&#xff0c;复制到tomcat/webapps目录即完成部署。2.使用tomcat-maven插件&#xff0c;在pom.xml的…

word 论文排版 —— 按指定格式章节的自动编号

在word中如何实现章节标题自动编号 标题样式与标题的编号是两个步骤&#xff0c;为标题建立编号是在为标题样式确定的基础后进行的。这是显而易见的&#xff0c;也即只有先定义了多级标题&#xff08;也可使用 word 自带的标题样式&#xff09;&#xff0c;才可为这些多级标题自…

北斗有 35 颗卫星,而 GPS 有 24 颗卫星,为什么二者数量不同?

作者&#xff1a;知乎用户链接&#xff1a;https://www.zhihu.com/question/21092045/answer/17164418来源&#xff1a;知乎著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。反对目前的两个不靠谱回答&#xff01;需要的卫星数目和别人占坑没有…

sharesdk短信验证码的集成

在ShareSDK官网http://mob.com/注册并创建Android应用.申请APP_key,下载SDK等 根据官网开发文档导入SDK,目录结构如下 将以上文件按需放入Android Studio项目所要使用SMSSDK的Module所在的Libs里面&#xff0c;再在Module的build.gradle里面将libs加入仓库&#xff08;reposito…

java四神兽_SpringCloud五大神兽之Eureka

注册中心概述什么是注册中心&#xff1f;相当于服务之间的‘通讯录’&#xff0c;记录了服务和服务地址之间的映射关系。在分布式架构中服务会注册到这里。当服务需要调用其他服务时&#xff0c;就在注册中心找到其他服务的地址&#xff0c;进行调用注册中心的主要作用&#xf…

windows下dubbo-admin和zookeeper安装部署

1. 概述 ZooKeeper是Hadoop的正式子项目&#xff0c;它是一个针对大型分布式系统的可靠协调系统&#xff0c;提供的功能包括&#xff1a;配置维护、名字服务、分布式同步、组服务等。ZooKeeper的目标就是封装好复杂易出错的关键服务&#xff0c;将简单易用的接口和性能高效、…

jpush java api_JPush極光推送Java服務器端API

產品功能說明極光推送(JPush)是一個端到端的推送服務&#xff0c;使得服務器端消息能夠及時地推送到終端用戶手機上&#xff0c;讓開發者積極地保持與用戶的連接&#xff0c;從而提高用戶活躍度、提高應用的留存率。極光推送客戶端支持 Android, iOS 兩個平台。本 Android SDK …

Maven详解(转)

转自 https://www.cnblogs.com/hongwz/p/5456578.html 一.前言 以前做过的项目中&#xff0c;没有真正的使用过Maven&#xff0c;只知道其名声很大&#xff0c;其作用是用来管理jar 包的。最近一段时间在项目过程中使用Maven&#xff0c;用Maven构建的web项目&#xff0c;其项目…

java商品编写代码_商品品牌业务之后台Java代码的编写

今天是刘小爱自学Java的第146天。感谢你的观看&#xff0c;谢谢你。商品品牌业务之后台Java代码的编写-1.jpg (50.46 KB, 下载次数: 1)2021-2-5 00:22 上传学习计划安排如下&#xff1a;昨天实现了前端页面的编写以及发送请求&#xff0c;今天关于异步请求工具axios的简单说明。…

java中8种数据类型和默认值所占字节数

java 8种基本数据类型的默认值及所占字节数通过一段代码来测试一下 8种基本数据类型的默认值 1 package dierge;2 3 public class Ceshi { 4 int a; 5 double b; 6 boolean c; 7 char d; 8 float f; 9 byte e; 10 long h; 11 short j; 12 public static void main(String…

软件测试实验1:为三角形问题编写一个Java程序,并用Junit测试程序

实验报告 实验内容&#xff1a; 1.Install Junit(4.12), Hamcrest(1.3) with Eclipse 2.Install Eclemma with Eclipse 3.Write a java program for the triangle problem and test the program with Junit. 实验步骤&#xff1a; 1.在eclipse中安装Junit、Hamcrest和eclemma。…

SurfaceFlinger与Surface概述

基本原理&#xff1a; SF一个Client对应一个app中的SurfaceComposerClient&#xff0c; 分别是Binder的n端和b端&#xff0c;主要用来CreateSurface 一个app中有多个Activity&#xff0c;一个Activity一般有一个Surface(除SurfaceView&#xff0c;一个View就有一个Surface)&…

拼图项目动手指南

Jigsaw项目将把模块化引入Java平台&#xff0c;根据原始计划&#xff0c;它将在12月10日完成功能。 所以我们在这里&#xff0c;但是拼图在哪里&#xff1f; 在过去的六个月中肯定发生了很多事情&#xff1a; 原型问世 &#xff0c;内部API的迫在眉睫的删除引起了很大的骚动 &…

java实现回文验证_LeetCode 精选 TOP 面试题(Java 实现)—— 验证回文串

文章目录一、题目描述1.1 题目验证回文串给定一个字符串&#xff0c;验证它是否是回文串&#xff0c;只考虑字母和数字字符&#xff0c;可以忽略字母的大小写。说明&#xff1a;本题中&#xff0c;我们将空字符串定义为有效的回文串。示例 1:输入: "A man, a plan, a cana…