通过Java 线程堆栈进行性能瓶颈分析

改善性能意味着用更少的资源做更多的事情。为了利用并发来提高系统性能,我们需要更有效的利用现有的处理器资源,这意味着我们期望使 CPU 尽可能出于忙碌状态(当然,并不是让 CPU 周期出于应付无用计算,而是让 CPU 做有用的事情而忙)。如果程序受限于当前的 CPU 计算能力,那么我们通过增加更多的处理器或者通过集群就能提高总的性能。总的来说,性能提高,需要且仅需要解决当前的受限资源,当前受限资源可能是:

CPU: 如果当前 CPU 已经能够接近 100% 的利用率,并且代码业务逻辑无法再简化,那么说明该系统的性能以及达到上线,只有通过增加处理器来提高性能

其他资源:比如连接数等。可以修改代码,尽量利用 CPU,可以获得极大的性能提升

如果你的系统有如下的特点,说明系统存在性能瓶颈:

随着系统逐步增加压力,CPU 使用率无法趋近 100%(如下图)

持续运行缓慢。时常发现应用程序运行缓慢。通过改变环境因子(负载,连接数等)也无法有效提升整体响应时间

系统性能随时间的增加逐渐下降。在负载稳定的情况下,系统运行时间越长速度越慢。可能是由于超出某个阈值范围,系统运行频繁出错从而导致系统死锁或崩溃

系统性能随负载的增加而逐渐下降。

一个好的程序,应该是能够充分利用 CPU 的。如果一个程序在单 CPU 的机器上无论多大压力都不能使 CPU 使用率接近 100%,说明这个程序设计有问题。一个系统的性能瓶颈分析过程大致如下:

先进性单流程的性能瓶颈分析,受限让单流程的性能达到最优。

进行整体性能瓶颈分析。因为单流程性能最优,不一定整个系统性能最优。在多线程场合下,锁争用㩐给也会导致性能下降。

高性能在不同的应用场合下,有不同的含义:

有的场合高性能意味着用户速度的体验,如界面操作等

有的场合,高吞吐量意味着高性能,如短信或者彩信,系统更看重吞吐量,而对每一个消息的处理时间不敏感

有的场合,是二者的结合

性能调优的终极目标是:系统的 CPU 利用率接近 100%,如果 CPU 没有被充分利用,那么有如下几个可能:

施加的压力不足

系统存在瓶颈

1 常见的性能瓶颈

1.1 由于不恰当的同步导致的资源争用

1.1.1 不相关的两个函数,公用了一个锁,或者不同的共享变量共用了同一个锁,无谓地制造出了资源争用

下面是一种常见的错误

classMyClass{    Object sharedObj;synchronizedfun1() {...}//    访问共享变量 sharedObjsynchronizedfun2() {...}//    访问共享变量 sharedObjsynchronizedfun3() {...}//    不访问共享变量  sharedObjsynchronizedfun4() {...}//    不访问共享变量  sharedObjsynchronizedfun5() {...}//    不访问共享变量  sharedObj}

上面的代码将 synchronized 加在类的每一个方法上面,违背了保护什么锁什么的原则。对于无共享资源的方法,使用了同一个锁,人为造成了不必要的等待。Java 缺省提供了 this 锁,这样很多人喜欢直接在方法上使用 synchronized 加锁,很多情况下这样做是不恰当的,如果不考虑清楚就这样做,很容易造成锁粒度过大:

两个不相干的方法(没有使用同一个共享变量),共用了 this 锁,导致人为的资源竞争

即使一个方法中的代码也不是处处需要锁保护的。如果整个方法使用了 synchronized,那么很可能就把 synchronized 的作用域给人为扩大了。在方法级别上加锁,是一种粗犷的锁使用习惯。

上面的代码应该变成下面

classMyClass{Object sharedObj;synchronizedfun1() {...}//    访问共享变量 sharedObjsynchronizedfun2() {...}//    访问共享变量 sharedObjfun3() {...}//    不访问共享变量  sharedObjfun4() {...}//    不访问共享变量  sharedObjfun5() {...}//    不访问共享变量  sharedObj}

1.1.2 锁的粒度过大,对共享资源访问完成后,没有将后续的代码放在synchronized 同步代码块之外

这样会导致当前线程占用锁的时间过长,其他需要锁的线程只能等待,最终导致性能受到极大影响

voidfun1(){synchronized(lock) {    ......//正在访问共享资源......//做其他耗时操作,但这些耗时操作与共享资源无关}}

上面的代码,会导致一个线程长时间占有锁,而在这么长的时间里其他线程只能等待,这种写法在不同的场合下有不同的提升余地:

单 CPU 场合 将耗时操作拿到同步块之外,有的情况下可以提升性能,有的场合则不能:

同步块的耗时代码是 CPU 密集型代码(纯 CPU 运算等),不存在磁盘 IO/网络 IO 等低 CPU 消耗的代码,这种情况下,由于 CPU 执行这段代码是 100% 的使用率,因此缩小同步块也不会带来任何性能上的提升。但是,同时缩小同步块也不会带来性能上的下降

同步块中的耗时代码属于磁盘/网络 IO等低 CPU 消耗的代码,当当前线程正在执行不消耗 CPU 的代码时,这时候 CPU 是空闲的,如果此时让 CPU 忙起来,可以带来整体性能上的提升,所以在这种场景下,将耗时操作的代码放在同步之外,肯定是可以提高整个性能的(?)

多 CPU 场合 将耗时的操作拿到同步块之外,总是可以提升性能

同步块的耗时代码是 CPU 密集型代码(纯 CPU 运算等),不存在磁盘 IO/网络 IO 等低 CPU 消耗的代码,这种情况下,由于是多 CPU,其他 CPU也许是空闲的,因此缩小同步块可以让其他线程马上得到执行这段代码,可以带来性能的提升

同步块中的耗时代码属于磁盘/网络 IO等低 CPU 消耗的代码,当当前线程正在执行不消耗 CPU 的代码时,这时候总有 CPU 是空闲的,如果此时让 CPU 忙起来,可以带来整体性能上的提升,所以在这种场景下,将耗时操作的代码放在同步块之外,肯定是可以提高整个性能的

不管如何,缩小同步范围,对系统没有任何不好的影响,大多数情况下,会带来性能的提升,所以一定要缩小同步范围,因此上面的代码应该改为

voidfun1(){synchronized(lock) {......//正在访问共享资源}......//做其他耗时操作,但这些耗时操作与共享资源无关}

1.1.3 其他问题

Sleep 的滥用,尤其是轮询中使用 sleep,会让用户明显感觉到延迟,可以修改为 notify 和 wait

String + 的滥用,每次 + 都会产生一个临时对象,并有数据的拷贝

不恰当的线程模型

效率地下的 SQL 语句或者不恰当的数据库设计

不恰当的 GC 参数设置导致的性能低下

线程数量不足

内存泄漏导致的频繁 GC

2.2 性能瓶颈分析的手段和工具

上面提到的这些原因形成的性能瓶颈,都可以通过线程堆栈分析,找到根本原因。

2.2.1 如何去模拟,发现性能瓶颈

性能瓶颈的几个特征:

当前的性能瓶颈只有一处,只有当解决了这一处,才知道下一处。没有解决当前性能瓶颈,下一处性能瓶颈是不会出现的。如下图所示,第二段是瓶颈,解决第二段的瓶颈后,第一段就变成了瓶颈,如此反复找到所有的性能瓶颈

性能瓶颈是动态的,低负载下不是瓶颈的地方,高负载下可能成为瓶颈。由于 JProfile 等性能剖析工具依附在 JVM 上带来的开销,使系统根本就无法达到该瓶颈出现时需要的性能,因此在这种场景下线程堆栈分析才是一个真正有效的方法

鉴于性能瓶颈的以上特点,进行性能模拟的时候,一定要使用比系统当前稍高的压力下进行模拟,否则性能瓶颈不会出现。具体步骤如下:

2.2.2 如何通过线程堆栈识别性能瓶颈

通过线程堆栈,可以很容易的识别多线程场合下高负载的时候才会出现的性能瓶颈。一旦一个系统出现性能瓶颈,最重要的就是识别性能瓶颈,然后根据识别的性能瓶颈进行修改。一般多线程系统,先按照线程的功能进行归类(组),把执行相同功能代码的线程作为一组进行分析。当使用堆栈进行分析的时候,以这一组线程进行统计学分析。如果一个线程池为不同的功能代码服务,那么将整个线程池的线程作为一组进行分析即可。

软件的多线程技术以及高并发问题是程序员绕不开的话题,想要了解更多多线程知识点的,可以关注我一下,另外顺便给大家推荐一个交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,以下的知识脑图也是在群里面获取的。

一般一个系统一旦出现性能瓶颈,从堆栈上分析,有如下三种最为典型的堆栈特征:

绝大多数线程的堆栈都表现为在同一个调用上下文,且只剩下非常少的空闲线程。可能的原因如下:

线程的数量过少

锁的粒度过大导致的锁竞争

资源竞争

锁范围中有大量耗时操作

远程通信的对方处理缓慢

绝大多数线程出于等待状态,只有几个工作的线程,总体性能上不去。可能的原因是,系统存在关键路径,关键路径已经达到瓶颈

线程总的数量很少(有些线程池的实现是按需创建线程,可能程序中创建线程

2.2.3 其他提高性能的方法

减少锁的粒度,比如 ConcurrentHashMap 的实现默认使用 16 个锁的 Array(有一个副作用:锁整个容器会很费力,可以添加一个全局锁)

2.2.4 性能调优的终结条件

性能调优总有一个终止条件,如果系统满足如下两个条件,即可终止:

算法足够优化

没有线程/资源的使用不当而导致的 CPU 利用不足

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,299评论 8 265
  • 前言:虽然自己平时都在用多线程,也能完成基本的工作需求,但总觉得,还是对线程没有一个系统的概念,所以,查阅了一些资...
    justCode_阅读 699评论 0 9
  • 关于时光的事儿。 昨天听了一个听书的内容是关于海伦凯勒的,主要是关于她如何学习语言文字的事情。今天上课一天的和动力...
    祥祥布鲁斯阅读 276评论 0 0
  • 我属鸡,他属狗,也许是别人常说的"鸡犬不宁"吧!我和前夫在一起二十年,也走不到对方的心里,终于在三年前,心平气和的...
    晗君阅读 210评论 0 0
  • 很感谢自己年轻时候所犯的错误,它们带给了我成长,让我可以不断地反思自己,让我可以遇见最好的自己。 第一个就是:我一...
    韩旭东阅读 285评论 1 3