改善性能意味着用更少的资源做更多的事情. 为了利用并发来提高系统性能, 我们需要更有效的利用现有的处理器资源, 这意味着我们期望使 CPU 尽可能出于忙碌状态(当然, 并不是让 CPU 周期出于应付无用计算, 而是让 CPU 做有用的事情而忙). 如果程序受限于当前的 CPU 计算能力, 那么我们通过增加更多的处理器或者通过集群就能提高总的性能. 总的来说, 性能提高, 需要且仅需要解决当前的受限资源, 当前受限资源可能是:
CPU: 如果当前 CPU 已经能够接近 100% 的利用率, 并且代码业务逻辑无法再简化, 那么说明该系统的性能以及达到上线, 只有通过增加处理器来提高性能
其他资源: 比如连接数等. 可以修改代码, 尽量利用 CPU, 可以获得极大的性能提升
如果你的系统有如下的特点, 说明系统存在性能瓶颈:
随着系统逐步增加压力, CPU 使用率无法趋近 100%(如下图)
持续运行缓慢. 时常发现应用程序运行缓慢. 通过改变环境因子 (负载, 连接数等) 也无法有效提升整体响应时间
系统性能随时间的增加逐渐下降. 在负载稳定的情况下, 系统运行时间越长速度越慢. 可能是由于超出某个阈值范围, 系统运行频繁出错从而导致系统死锁或崩溃
系统性能随负载的增加而逐渐下降.
一个好的程序, 应该是能够充分利用 CPU 的. 如果一个程序在单 CPU 的机器上无论多大压力都不能使 CPU 使用率接近 100%, 说明这个程序设计有问题. 一个系统的性能瓶颈分析过程大致如下:
先进性单流程的性能瓶颈分析, 受限让单流程的性能达到最优.
进行整体性能瓶颈分析. 因为单流程性能最优, 不一定整个系统性能最优. 在多线程场合下, 锁争用给也会导致性能下降.
高性能在不同的应用场合下, 有不同的含义:
有的场合高性能意味着用户速度的体验, 如界面操作等
有的场合, 高吞吐量意味着高性能, 如短信或者彩信, 系统更看重吞吐量, 而对每一个消息的处理时间不敏感
有的场合, 是二者的结合
性能调优的终极目标是: 系统的 CPU 利用率接近 100%, 如果 CPU 没有被充分利用, 那么有如下几个可能:
施加的压力不足
系统存在瓶颈
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 如何通过线程堆栈识别性能瓶颈
通过线程堆栈, 可以很容易的识别多线程场合下高负载的时候才会出现的性能瓶颈. 一旦一个系统出现性能瓶颈, 最重要的就是识别性能瓶颈, 然后根据识别的性能瓶颈进行修改. 一般多线程系统, 先按照线程的功能进行归类(组), 把执行相同功能代码的线程作为一组进行分析. 当使用堆栈进行分析的时候, 以这一组线程进行统计学分析. 如果一个线程池为不同的功能代码服务, 那么将整个线程池的线程作为一组进行分析即可.
一般一个系统一旦出现性能瓶颈, 从堆栈上分析, 有如下三种最为典型的堆栈特征:
绝大多数线程的堆栈都表现为在同一个调用上下文, 且只剩下非常少的空闲线程. 可能的原因如下:
线程的数量过少
锁的粒度过大导致的锁竞争
资源竞争
锁范围中有大量耗时操作
远程通信的对方处理缓慢
绝大多数线程出于等待状态, 只有几个工作的线程, 总体性能上不去. 可能的原因是, 系统存在关键路径, 关键路径已经达到瓶颈
线程总的数量很少(有些线程池的实现是按需创建线程, 可能程序中创建线程
一个例子
"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 性能调优的终结条件
性能调优总有一个终止条件, 如果系统满足如下两个条件, 即可终止:
算法足够优化
没有线程 / 资源的使用不当而导致的 CPU 利用不足
来源: http://www.tuicool.com/articles/6VVnUz3