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

  • CPU: 如果当前 CPU 已经能够接近 100% 的利用率,并且代码业务逻辑无法再简化,那么说明该系统的性能以及达到上线,只有通过增加处理器来提高性能
  • 其他资源:比如连接数等。可以修改代码,尽量利用 CPU,可以获得极大的性能提升

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

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

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

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

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

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

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

  1. 有的场合高性能意味着用户速度的体验,如界面操作等
  2. 有的场合,高吞吐量意味着高性能,如短信或者彩信,系统更看重吞吐量,而对每一个消息的处理时间不敏感
  3. 有的场合,是二者的结合

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

  1. 施加的压力不足
  2. 系统存在瓶颈

1 常见的性能瓶颈

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

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

下面是一种常见的错误

class MyClass {
Object sharedObj;
synchronized fun1() {...} // 访问共享变量 sharedObj
synchronized fun2() {...} // 访问共享变量 sharedObj
synchronized fun3() {...} // 不访问共享变量 sharedObj
synchronized fun4() {...} // 不访问共享变量 sharedObj
synchronized fun5() {...} // 不访问共享变量 sharedObj
}

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

  • 两个不相干的方法(没有使用同一个共享变量),共用了 this 锁,导致人为的资源竞争
  • 即使一个方法中的代码也不是处处需要锁保护的。如果整个方法使用了 synchronized,那么很可能就把 synchronized 的作用域给人为扩大了。在方法级别上加锁,是一种粗犷的锁使用习惯。

上面的代码应该变成下面

class MyClass {
Object sharedObj;
synchronized fun1() {...} // 访问共享变量 sharedObj
synchronized fun2() {...} // 访问共享变量 sharedObj
fun3() {...} // 不访问共享变量 sharedObj
fun4() {...} // 不访问共享变量 sharedObj
fun5() {...} // 不访问共享变量 sharedObj
}

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

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

void fun1()
{
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 忙起来,可以带来整体性能上的提升,所以在这种场景下,将耗时操作的代码放在同步块之外,肯定是可以提高整个性能的

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

void fun1()
{
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性能优化这些成为架构师必备的知识体系,以下的知识脑图也是在群里面获取的。

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

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

    • 线程的数量过少
    • 锁的粒度过大导致的锁竞争
    • 资源竞争
    • 锁范围中有大量耗时操作
    • 远程通信的对方处理缓慢
  2. 绝大多数线程出于等待状态,只有几个工作的线程,总体性能上不去。可能的原因是,系统存在关键路径,关键路径已经达到瓶颈
  3. 线程总的数量很少(有些线程池的实现是按需创建线程,可能程序中创建线程

一个例子

"Thread-243" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
[0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at oracle.net.ns.Packet.receive(Unknown Source)
... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes()
at oracle.jdbc.driver.OracleResultSetImpl.getBytes()
- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)
at oracle.jdbc.driver.OracleResultSet.getBytes(O)
... ...
at org.hibernate.loader.hql.QueryLoader.list()
at org.hibernate.hql.ast.QueryTranslatorImpl.list()
... ...
at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)
at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)
at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)
at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
at com.wes.threadpool.PooledExecutorEx$Worker.run()
at java.lang.Thread.run(Thread.java:595)
"Thread-248" prio=1 tid=0xa58f2048 nid=0x7ac2 runnable
[0xaeedb000..0xaeedc480]
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at oracle.net.ns.Packet.receive(Unknown Source)
... ...
at oracle.jdbc.driver.LongRawAccessor.getBytes()
at oracle.jdbc.driver.OracleResultSetImpl.getBytes()
- locked <0x9350b0d8> (a oracle.jdbc.driver.OracleResultSetImpl)
at oracle.jdbc.driver.OracleResultSet.getBytes(O)
... ...
at org.hibernate.loader.hql.QueryLoader.list()
at org.hibernate.hql.ast.QueryTranslatorImpl.list()
... ...
at com.wes.NodeTimerOut.execute(NodeTimerOut.java:175)
at com.wes.timer.TimerTaskImpl.executeAll(TimerTaskImpl.java:707)
at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
- locked <0x80df8ce8> (a com.wes.timer.TimerTaskImpl)
at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
at com.wes.threadpool.PooledExecutorEx$Worker.run()
at java.lang.Thread.run(Thread.java:595)
... ...
"Thread-238" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()
[0xaec56000..0xaec57700]
at java.lang.Object.wait(Native Method)
at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)
- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)
at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)
... ...
at org.hibernate.impl.SessionImpl.list()
at org.hibernate.impl.SessionImpl.find()
at com.wes.DBSessionMediatorImpl.find()
at com.wes.ResourceDBInteractorImpl.getCallBackObj()
at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)
at com.wes.timer.TimerTaskImpl.executeAll()
at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)
at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
at com.wes.threadpool.PooledExecutorEx$Worker.run()
at java.lang.Thread.run(Thread.java:595) "Thread-233" prio=1 tid=0xa4a84a58 nid=0x7abd in Object.wait()
[0xaec56000..0xaec57700] at java.lang.Object.wait(Native Method)
at com.wes.collection.SimpleLinkedList.poll(SimpleLinkedList.java:104)
- locked <0x6ae67be0> (a com.wes.collection.SimpleLinkedList)
at com.wes.XADataSourceImpl.getConnection_internal(XADataSourceImpl.java:1642)
... ...
at org.hibernate.impl.SessionImpl.list()
at org.hibernate.impl.SessionImpl.find()
at com.wes.DBSessionMediatorImpl.find()
at com.wes.ResourceDBInteractorImpl.getCallBackObj()
at com.wes.NodeTimerOut.execute(NodeTimerOut.java:152)
at com.wes.timer.TimerTaskImpl.executeAll()
at com.wes.timer.TimerTaskImpl.execute(TimerTaskImpl.java:627)
- locked <0x80e08c00> (a com.facilities.timer.TimerTaskImpl)
at com.wes.threadpool.RunnableWrapper.run(RunnableWrapper.java:209)
at com.wes.threadpool.PooledExecutorEx$Worker.run()
at java.lang.Thread.run(Thread.java:595)
... ...

从堆栈看,有 51 个(socket)访问,其中有 50 个是 JDBC 数据库访问。其他方法被阻塞在 java.lang.Object.wait() 方法上。

2.2.3 其他提高性能的方法

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

2.2.4 性能调优的终结条件

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

  1. 算法足够优化
  2. 没有线程/资源的使用不当而导致的 CPU 利用不足

通过Java 线程堆栈进行性能瓶颈分析的更多相关文章

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

    改善性能意味着用更少的资源做更多的事情.为了利用并发来提高系统性能,我们需要更有效的利用现有的处理器资源,这意味着我们期望使 CPU 尽可能出于忙碌状态(当然,并不是让 CPU 周期出于应付无用计算, ...

  2. Java问题定位之Java线程堆栈分析

    采用Java开发的大型应用系统越来越大,越来越复杂,很多系统集成在一起,整个系统看起来像个黑盒子.系统运行遭遇问题(系统停止响应,运行越来越慢,或者性能低下,甚至系统宕掉),如何速度命中问题的根本原因 ...

  3. Java线程堆栈分析

    不知觉间工作已有一年了,闲下来的时候总会思考下,作为一名Java程序员,不能一直停留在开发业务使用框架上面.老话说得好,机会是留给有准备的人的,因此,开始计划看一些Java底层一点的东西,尝试开始在学 ...

  4. Java项目性能瓶颈分析及定位(八)——Java线程堆栈分析(五)

    对于CPU而言,常见的瓶颈主要有两种:服务器的压力很小,但是CPU的利用率却很高,这样的性能瓶颈相对比较容易定位(好比我只是说了你一句,你就哭了,你的弱点立马就暴露出来了):给服务器施加的压力很大,但 ...

  5. Java问题定位之如何借助线程堆栈进行问题分析

    在大型的应用中,线程堆栈打印出来特别多,如何从众多的信息中找到真正有用,有价值的信息,我们需要一定的技巧.本文对此详细介绍. 我们可以从三个方面分析:堆栈的局部信息,一次堆栈的统计信息,多个堆栈的对比 ...

  6. 怎样分析java线程堆栈日志

    注: 该文章的原文是由 Tae Jin Gu 编写,原文地址为 How to Analyze Java Thread Dumps 当有障碍,或者是一个基于 JAVA 的 WEB 应用运行的比预期慢的时 ...

  7. Java线程池使用和分析(一)

    线程池是可以控制线程创建.释放,并通过某种策略尝试复用线程去执行任务的一种管理框架,从而实现线程资源与任务之间的一种平衡. 以下分析基于 JDK1.7 以下是本文的目录大纲: 一.线程池架构 二.Th ...

  8. Java线程池使用和分析(二) - execute()原理

    相关文章目录: Java线程池使用和分析(一) Java线程池使用和分析(二) - execute()原理 execute()是 java.util.concurrent.Executor接口中唯一的 ...

  9. Java 线程池(ThreadPoolExecutor)原理分析与使用

    在我们的开发中"池"的概念并不罕见,有数据库连接池.线程池.对象池.常量池等等.下面我们主要针对线程池来一步一步揭开线程池的面纱. 使用线程池的好处 1.降低资源消耗 可以重复利用 ...

随机推荐

  1. empty是判断一个变量是否为“空”,而isset 则是判断一个变量是否已经设置

    1.echo和print的区别php中echo和print的功能基本相同(输出),但是两者之间还是有细微差别的.echo输出后没有返回值,但print有返回值,当其执行失败时返回flase.因此可以作 ...

  2. deque Comparison of Queue and Deque methods Comparison of Stack and Deque methods

    1. 队列queue和双端队列deque的转换 Queue Method Equivalent Deque Methodadd(e) addLast(e)offer(e) offerLast(e)re ...

  3. time out 超时

    网络不通:比如代理服务拒绝连接 网络ok,但是数据量过大,传输超时

  4. 单机闭环 使用Nginx+Lua开发高性能Web应用

    [西域骆驼D1532101213]西域骆驼(VANCAMEL)D1532101213 休闲套脚鞋 卡其43[行情 报价 价格 评测]-京东 http://item.jd.com/1856564.htm ...

  5. textField placeholder颜色,位置设置

    自定义textField继承自UITextField 重写 - (CGRect)placeholderRectForBounds:(CGRect)bounds _phoneTF.font = HPFo ...

  6. Spring Data JPA(官方文档翻译)

    关于本书 介绍 关于这本指南 第一章 前言 第二章 新增及注意点 第三章 项目依赖 第四章 使用Spring Data Repositories 4.1 核心概念 4.2 查询方法 4.3 定义rep ...

  7. 币安Binance API

    本文介绍币安Binance API General API Information The base endpoint is: https://api.binance.com All endpoint ...

  8. MongoDB的数据恢复

    当MongoDB正在插入或更新数据时,若突然出现断电或者不可逆转的摧毁性事件发生时,MongoDB没有像oracle或sql server这种关系型数据库提供事物机制,所以会产生垃圾数据.但Mongo ...

  9. 【linux set命令】shell bash 打印执行的命令

    在文件开头加上 set -x 可以打印执行的命令,可以用于调试 set 命令使用方法 https://www.jianshu.com/p/ea406382be3e

  10. 洛谷P3209平面图判定 [HNOI2010] 2-sat

    正解:2-sat(并茶几/强连通分量 解题报告: 传送门w 难受死了,连WA5次,正确率又-=INF了QAQ 然后先说下这题怎么做再来吐槽自己QAQ 首先这题其实和NOIp2010的关押罪犯挺像的,然 ...