Java性能权威指南-总结16

news2024/11/20 20:44:13

Java性能权威指南-总结16

  • 线程与同步的性能
    • 线程池与ThreadPoolExecutor
      • 设置最大线程数
      • 设置最小线程数

线程与同步的性能

能够轻松编写多线程程序也是Java的一个标志性特征。Java性能方面的吸引力显而易见:如果有两个CPU可用,那么一个应用能够完成的工作量可能是原来的2倍,或者是以快1倍的时间完成相同的工作量。当然,这是在假设任务可以分解成离散的片段的前提之下的,因为Java不是能自动找出算法性部分并实现并行化的语言。今日所见之计算,往往是离散性的任务:服务器处理来自离散的客户端的同步请求,批处理作业在一系列数据上执行相同的操作,数学算法可以分节成多个组成部分,诸如此类。此部分探讨的主题是,如何挖掘出Java线程和同步设施的最大性能。

线程池与ThreadPoolExecutor

在Java中,线程可以使用定制的代码来管理;应用也可以利用线程池。Java EE应用服务器就是围绕用一个或多个线程池处理请求这一概念构建的:对服务器上Servlet的每个调用都是通过池中的线程处理的(也有可能不同)。类似地,其他应用可以使用Java的ThreadPoolExecutor并行执行任务。

有些Java EE应用服务器就是使用ThreadPoolExecutor类的实例来管理其任务的,尽管有些应用服务器编写了自己的线程池,不过一般也仅仅是因为当时Java API中还没有加入ThreadPoolExecutor类而已。不过在这些情况下,线程池的实现可能会有所不同,但基本概念是一样的。在使用线程池时,有一个因素非常关键:调节线程池的大小对获得最好的性能至关重要。线程池的性能会随线程池大小这一基本选择而有所不同,在某些条件下,线程池过大对性能也有很大的不利影响。

所有线程池的工作方式本质是一样的:**有一个队列,任务被提交到这个队列中。(可以有不止一个队列,概念是一样的。)一定数量的线程会从该队列中取任务,然后执行。**任务的结果可以发回客户端(比如应用服务器的情况下),或保存到数据库中,或保存到某个内部数据结构中,等等。但是在执行完任务后,这个线程会返回任务队列,检索另一个任务并执行(如果没有更多任务要执行,该线程会等待下一个任务)。

线程池有最小线程数和最大线程数。池中会有最小数目的线程随时待命,等待任务指派给它们。因为创建线程的成本非常高昂,这样可以提高任务提交时的整体性能:已有的线程会拿到该任务并处理。另一方面,线程需要一些系统资源,包括栈所需的原生内存,如果空闲线程太多,就会消耗本来可以分配给其他进程的资源。最大线程数还是一个必要的限流阀,防止一次执行太多线程。

ThreadPoolExecutor和相关的类将最小线程数称作核心池大小,大部分应用服务器会使用类似minimum(最小值)的术语(如MinThreads)。它们是同一个概念。然而,在决定何时调整线程池大小的方式上,ThreadPoolExecutor和大部分Java EE应用服务器有些重要的差别。本节后面会探讨这些差别。现在,考虑ThreadPooLExecutor的最简单的情况,大部分Java EE应用服务器也是这么工作的:如果有个任务要执行,而所有的并发线程都在忙于执行另一个任务,就启动一个新线程(直到创建的线程达到最大线程数)。

设置最大线程数

先来设定最大线程数:对于给定硬件上的给定负载,最大线程数设置为多少最好?这个问题回答起来并不简单;它取决于负载特性以及底层硬件。特别是,最优线程数还与每个任务阻塞的频率有关。

为方便讨论,假设JVM有4个CPU可用。至于是系统只有4个CPU,还是说有128个硬件线程但本次只想利用其中的4个,并不重要,因为目标就是最大化这4个CPU的利用率。

很明显,最大线程数至少要设置为4。的确,除了处理这些任务,JVM中还有些线程要做其他的事,但是它们几乎从来不会占用一个完整的CPU。

如果线程数多于4,会有帮助吗?这时就要看负载特性了。考虑最简单的情况,假定任务都是计算密集型的:没有外部网络调用(比如不会访问数据库),也不会激烈地竞争内部锁。在使用模实体管理器(mock entity manager)的情况下,股价历史批处理程序就是一个这样的应用:实体上的数据完全可以并行计算。

下面就使用线程池计算一下10000个模股票实体的历史,假设机器有4个CPU,使用不同的线程数测试,具体的性能数据见下表。如果池中只有1个线程,计算数据集需要255.6秒;用4个线程,则只需要77秒。如果线程数超过4个,随着线程数的增加,需要的时间会稍多一些。
在这里插入图片描述
如果应用中的任务是完全并行的,则在有2个线程时,“与基准的百分比”这列为50%;在有4个线程时,这列为25%。但是这种完全线性的比例不可能出现,原因有这么几点:如果没有其他线程帮助,这些线程必须自己来协同,实现从运行队列中选取任务(一般而言,通常会有更多同步)。到了使用4个线程的时候,系统会100%消耗可用的CPU,尽管机器可能没有运行其他用户级的应用,但是会有各种系统级的进程进来,并使用CPU,从而使得JVM无法100%地使用所有CPU周期。

这个应用在伸缩性方面表现还不错,且即使池中的线程数被显著高估,性能损失也比较轻微。不过在其他情况下,性能损失可能会很大。在Servlet版的股票历史计算程序中,线程太多的话,影响会很大,如下表所示。应用服务器分别配置成不同的线程数,有一个负载生成器会向该服务器发送20个同步的(simultaneous)请求。
在这里插入图片描述
鉴于应用服务器有4个CPU可用,最大吞吐量可以通过将池中的线程数设置为4来实现。在研究性能问题时确定瓶颈在哪儿比较重要。 在这个例子中,瓶颈很明显是CPU:4个线程时,CPU利用率为100%。不过加入更多线程的影响其实很小,至少当线程数是原来的8倍时才会有明显的差别。

如果瓶颈在其他地方呢?这个例子有点不同寻常,任务完全是CPU密集型的:没有I/O。一般来说,线程有可能会调用数据库,或者把输出写到某个地方,甚至是会合其他某些资源。在那种情况下,瓶颈未必是CPU,而可能是外部资源。对于此类情况,添加线程非常有害。

仍以股票Servlet为例,把目标变一下:如果目标是最大限度地利用负载生成器机器,又会如何,是简单地运行一个多线程的Java程序吗?在典型的用法中,如果Servlet应用运行在一个有4个CPU的应用服务器上,而且只有一个客户端请求数据,那么,应用服务器大约会25%忙碌,客户端机器几乎总是空闲的。如果负载增加到4个并发的客户端,则应用服务器会100%忙碌,客户端机器可能只有20%的忙碌。

只看客户端,很容易得出这样的结论:因为客户端CPU大量过剩,应该可以添加更多线程,改善其伸缩性。下表说明了这种假设何其错误:当客户端再加入一些线程时,性能会受到极大影响。
在这里插入图片描述
在这个例子中,一旦应用服务器成为瓶颈(也就是说,线程数达到4个时),向服务器增加负载是非常有害的——即使只是在客户端加了几个线程。这个例子看上去可能有点有意为之。如果服务器已经是CPU密集型的,谁还会加入更多线程呢?之所以使用这个例子,只是因为它容易理解,而且仅使用了Java程序。这意味着自己就可以运行,并理解它是如何工作的,而不必设置数据库连接、模式(Schema)等选项。

需要指出的是,对于还要向CPU密集型或I/O密集型的机器发送数据库请求的应用服务器而言,同样的原则也成立。你可能只关注应用服务器CPU,看到小于100%就感觉不错;看到有多余的请求要处理,就假定增加应用服务器的线程数是个不错的主意。结果会让人大吃一惊,因为在那种情况下增加线程数,实际上会降低整体吞吐量(影响可能非常明显),就像前面那个只有Java程序的例子一样。了解系统真正瓶颈之所在非常重要的另一个原因是:如果还向瓶颈处增加负载,性能会显著下降。相反,如果减少了当前瓶颈处的负载,性能可能会上升。

这也是设计自我调优的线程池非常困难的原因所在。线程池通常对挂起了多少工作有所了解,甚至有多少CPU可用也可以知道,但是它们通常看不到所在的整个环境的其他方面。因此,当有工作挂起时,增加线程(这是很多自我调优的线程池的一个核心特性,也是ThreadPoolExecutor的某些配置)往往是完全错误的。

遗憾的是,设置最大线程数更像是艺术而非科学,原因也在于此。在现实中,测试条件下自我调优的线程池会实现可能性能的80%~90%;而且就算高估了所需线程数,也可能只有很小的损失。但是当设置线程数大小这方面出了问题时,系统可能会在很大程度上出现问题。就此而言,充足的测试仍然非常关键。

设置最小线程数

一旦确定了线程池的最大线程数,就该确定所需的最小线程数了。大部分情况下,开发者会直截了当地将它们设置为同一个值。

将最小线程数设置为其他某个值(比如1),出发点是防止系统创建太多线程,以节省系统资源。因为每个线程都需要一定量的内存,特别是线程的栈。所设置的系统大小应该能够处理预期的最大吞吐量,而要达到最大吞吐量,系统将需要创建所有那些线程。 如果系统做不到这一点,那选择一个最小线程数也没什么帮助:如果系统达到了这样的条件——需要按所设置的最大线程数启动所有线程,而又无法满足,系统将陷入困境。创建最终可能会需要的所有线程,并确保系统可以处理预期的最大负载,这样更好。

另一方面,指定一个最小线程数的负面影响相当小。如果第一次就有很多任务要执行,会有负面影响:这时线程池需要创建一个新线程。创建线程对性能不利,这也是为什么起初需要线程池的原因,不过这种一次性的成本在性能测试中很可能察觉不到(只要这个线程仍然在池中)。

在批处理应用中,线程是在创建线程池时分配(如果将最大线程数和最小线程数设置为同一个值,就会出现这种情况),还是按需分配,并不重要:执行应用所需的时间是一样的。在其他应用中,新线程可能会在预热阶段分配(分配线程的总时间还是一样的),对性能的影响可以忽略不计。即使线程创建发生在可以测量的周期内,只要此类操作有限,也很有可能测不出来。

另一个可以调优的地方是线程的空闲时间。比如,某个线程池的最小线程数为1,最大线程数为4。现在假设一般会有一个线程在执行,处理一个任务;然后应用进入这样一个循环:每15秒,负载平均有2个任务要执行。第一次进入这个循环时,线程池会创建第2个线程,此时,让这个新创建的线程在池中至少留存一段时间是有意义的。希望避免这种情况:第2个线程创建出来后,5秒钟内结束其任务,空闲5秒,然后退出了。而5秒之后又需要为下一个任务创建一个线程。一般而言,对于线程数为最小值的线程池,一个新线程一旦创建出来,至少应该留存几分钟,以处理任何负载飙升。如果任务到达率有个比较好的模型,可以基于这个模型设置空闲时间。另外,空闲时间应该以分钟计,而且至少在10分钟到30分钟之间。

留存一些空闲线程,对应用性能的影响通常微乎其微。一般而言,线程对象本身不会占用大量的堆空间。除非线程保持了大量的线程局部存储,或者线程的Runnable对象引用了大量内存。不管是哪种情况,释放这样的线程都会显著减少堆中的活数据(这反过来又会影响GC的效率)。

不过对线程池而言,这些情况并不多见。当池中的某个对象空闲时,它就不应该再引用任何Runnable对象(如果引用了,就说明哪个地方有bug了)。根据线程池的实现情况,线程局部变量可能会继续保留;尽管在某些情况下,线程局部变量可以有效促成对象重用,但是那些线程局部对象所占用的总的内存量,应该加以限制。

对于可能会增长到非常大(当然也是运行在规模很大的机器上)的线程池,这个规则有个重要的特例。举例而言,假设某个线程池的任务队列预计平均有20个任务,那么20就是很好的最小值。再假设这个池运行在一个规模很大的机器上,它被设计为可以处理2000个任务的峰值负载。如果在池中留存2000个空闲线程,则当只有20个任务时,对性能会有所影响:如果只有核心的20个线程忙碌,与有1980个空闲线程相比,前者的吞吐量可能是后者的50%。线程池一般不会遇到这样的问题,但如果遇到了,那就应该确认一下池的合适的最小值了。

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

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

相关文章

计算机网络笔记(更新中)

本文是个人笔记,都是概念,没基础不建议看。 绪论 计算机网络的定义 最简单的定义:计算机网络是一些互相连接的、自治的计算机的集合因特网(Internet)是“网络的网络” 计算机网络的组成(物理组成&#x…

STM32单片机(七)ADC模拟数字转换器----第一节:ADC模数转换器

❤️ 专栏简介:本专栏记录了从零学习单片机的过程,其中包括51单片机和STM32单片机两部分;建议先学习51单片机,其是STM32等高级单片机的基础;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 :适用于想要…

autoDL上A100运行wiki出错:NVIDIA A100-PCIE-40GB(最后安装好torch+dgl了);学校服务器加2.X版本pytorch

1、A100运行wiki出错:NVIDIA A100-PCIE-40GB with CUDA capability sm_80 is not compatible with the current PyTorch installation. The current PyTorch install supports CUDA capabilities sm_37 sm_50 sm_60 sm_70. If you want to use the NVIDIA A100-PCIE…

CTFshow-pwn入门-pwn26-pwn28

什么是ASLR 大多数的攻击都基于这样一个前提,即攻击者知道程序的内存布局,需要提前知道shellcode或者其他一些数据的位置。因此,引入内存布局的随机化能够有效增加漏洞利用的难度,其中一种技术就是ASLR(Address Space…

Qt颜色、文件、字体对话框

项目目录 界面 一、颜色对话框 QColor color QColorDialog::getColor(QColor(255,0,0));qDebug()<<"r"<<color.red()<<" g "<<color.green()<<" b "<<color.blue(); 二、文件对话框 //文件对话框QString…

《网络安全0-100》-网络攻击方式

网络攻击方式 DoS DDoS攻击 DoS攻击和DDoS攻击都是网络攻击的一种&#xff0c;它们的区别如下&#xff1a; DoS攻击(Denial of Service&#xff0c;拒绝服务攻击)&#xff1a;指攻击者通过向目标计算机或网络发送大量的合法请求&#xff0c;占用其网络资源和带宽&#xff0c;…

Java API

目录 1.JSON 2.多线程 3.网络编程 1.JSON &#xff08;1&#xff09;定义&#xff1a; 1.轻量级的文本数据交换格式 2.具有自我描述性【看到某个JSON数据就能知道它的特点】 3.比XML传输速度快【跨平台】 (2)语法规则&#xff1a; 1.语法&#xff1a; 2.构成要素&#xff…

Cortext-M3系列:M3内核概述(1)

引言&#xff1a; 本系列博客记录的是博主以前学习单片机时期的一些关于MCU内核方面的知识点和笔记&#xff0c;分享给一起学习的小伙伴&#xff0c;也作为复习用处。文中出现的Cortex-M3、CM3、M3均指的是ARM公司的产品Cortex-M3&#xff0c;后面不再赘述。本系列的博客仅仅只…

基于Java共享客栈管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

Go语言包设计指南

1. Go包的认知 1.1 Go包是基本功能单元 我们知道Go包是Go编程语言中的一个重要概念&#xff0c;它是一组相关的Go源代码文件。并且&#xff0c;在Go中&#xff0c;每个Go源文件都必须属于一个包。 Go包是一个逻辑上独立的单元&#xff0c;是Go的基本功能单元&#xff0c;用来做…

Redis安装说明(heima)

Redis安装说明&#xff08;heima&#xff09; 笔者的redis(Linux版)的下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87926624 笔者的redis图形化桌面客户端&#xff0c;下载链接地址为&#xff1a;https://download.csdn.net/download/weixin…

Python单元测试框架之pytest -- 断言

对于测试来讲&#xff0c;不管是功能测试&#xff0c;自动化测试&#xff0c;还是单元测试。一般都会预设一个正确的预期结果&#xff0c;而在测试执行的过程中会得到一个实际的结果。测试的成功与否就是拿实际的结果与预期的结果进行比较。这个比的过程实际就是断言&#xff0…

PHP实战开发24-一定要知道PHP中反射的强大应用

文章目录 一、前言1.1 关于反射1.2 PHP中的反射 二、反射的应用2.1 自动注入2.2 动态调用方法2.3 注解解析 总结 一、前言 本文已收录于PHP全栈系列专栏&#xff1a;PHP快速入门与实战 1.1 关于反射 反射是一种编程语言的特性&#xff0c;它允许程序在运行时获取和操作对象的…

kotlin从入门到精通之内置类型

基本类型 声明变量 val&#xff08;value的简写&#xff09;用来声明一个不可变的变量&#xff0c;这种变量在初始赋值之后就再也不能重新赋值&#xff0c;对应Java中的final变量。 var&#xff08;variable的简写&#xff09;用来声明一个可变的变量&#xff0c;这种变量在初始…

CUDA线程的线程层次结构,以及单个线程threadIdx如何使用stride来进行跳步操作,同时对多个数据进行计算

线程层次的概念&#xff1a; 简单说&#xff0c;就是一个grid有多个block,一个block有多个thread. grid有多大&#xff0c;用gridDim表示它有多少个block&#xff0c;具体分为gridDim.x, gridDim.y&#xff0c;gridDim.z。 block有多大&#xff0c;用blockDim表示它有多少个t…

SpringBoot项目结合@Slf4j将日志记录到磁盘和数据库

文章目录 1、背景介绍2、存本地2.1、配置文件2.2、使用 3、存数据库3.1、配置文件改造3.2、过滤器编写3.3、表准备3.4、添加依赖3.5、测试 4、优化4.1、日志定期删除4.2、分库处理4.3、环境 5、总结 1、背景介绍 现在我一个SpringBoot项目想记录日志&#xff0c;大概可以分为下…

深度学习助力版面分析技术,图像“还原”有方

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

【Python 随练】分数序列

题目&#xff1a; 有一分数序列&#xff1a;2/1&#xff0c;3/2&#xff0c;5/3&#xff0c;8/5&#xff0c;13/8&#xff0c;21/13… 求出这个数列的前 20 项之和。 简介&#xff1a; 在本篇博客中&#xff0c;我们将研究一个有趣的数学问题&#xff1a;求解一个特殊数列的…

MySQL出现Specified key was too long; max key length is 3072 bytes解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

算法篇——动态规划 01背包问题 (js版)

416. 分割等和子集 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 链接&#xff1a;力扣 解题思路&#xff1a; 这道题看似是比较简单的背包问题&#xff1a; 首先可以通过判断数组和是否是…