某些类型的 bug 经常落到性能调优师手中来进行修复,虽然严格地讲,它们算不上是性能问题。通常由对象泄漏造成的内存不足就是这类 bug 中的一个。(在本专栏前面的一期中介绍过如何在“垃圾对话(Trash talk)”中处理这些问题,请参阅 参考资料。)另外一类经常落到性能调优师手上修补的 bug 就是线程死锁和其他线程方面的问题,例如竟态条件,因为这些问题一般只在对程序进行负载测试时才会表现出来。
将这些 bug 交到性能调优师手中通常有很好的理由:识别和清除性能及内存瓶颈所需要的工具,与识别对象泄漏和竟态条件的工具相同。死锁相对来说比较容易识别;只要注意到应用程序冻结,就会有堆栈跟踪显示是哪套线程锁定了其他线程的监视器。但是不幸的是,对于竟态条件,有可能更加无从下手。
等待泄漏
有一类叫做 等待泄漏 的竟态条件最近受到我们的关注。基本的问题是,在使用 wait/notify 的概念时,通常会有一个或多个线程阻塞在 wait() 调用中,等待着另外一个线程通知它某个条件已经为真,这样它才能退出 wait() 调用,并继续进行处理。通知线程调用 notify() 或 notifyAll() 方法来通知等待线程现在就可以苏醒并继续进行处理。
这种方法很显然会形成竟态条件,但是一直到最近,我们在实践中都没有看到过这种情况。如果进入等待状态,等待特定资源变得可用,但是另外一个线程调用 notify() 正好是 在 您进入等待状态 之前 进行的,那么会发生什么呢?结果是:即使资源可用,线程也会陷入等待状态。
当然,有许多解决方案来避免这种场景 —— 毕竟,这是一个与其他 bug 类似的 bug。显然您应当更加仔细,在进入等待状态之前,判断资源是否可用。更具体来说,您应当检查资源在同步块内部是否可用,而不应当在资源可用的时候进入等待状态(这是推荐的方案,但这可能是一种可伸缩性较差的解决方案),或者也可以用 JDK 5.0 中可以使用的一些更复杂的同步类和相关的技术(参阅 参考资料)。
等待泄漏显然是个 bug,但是在这里要关心的不是对这个问题的解决方案,而是发现问题的方法。在拥有成百上千个线程的复杂应用程序中,除非事先发现故障现象,否则很难找出等待泄漏。与死锁不同,这里没有明显的能够说明问题的证据(例如两个线程相互等待对方锁定的监视器)。相反,会有大量线程滞留在 Object.wait() 调用中,对于许多应用程序来说,这是非常正常的情况。
模拟一个等待泄漏
学习如何发现等待泄漏的最好方法是实际查看一个泄漏,并理解导致它的原因。清单 1 演示了一个非常简单的等待泄漏。 WaitLeak 类实现了 Runnable,每个线程要停下来等待,直到得到通知,然后终止。在这个模拟中,启动了 4 个 WaitLeak 线程,每秒钟启动一个。另一个类 WaitLeakNotifier 通知所有在 WaitLeak wait() 调用中等待的线程,然后终止。主方法接受一个参数,该参数表示 WaitLeakNotifier 通知所有等待线程之前等待的毫秒数。
清单 1. 等待泄漏模拟类
public class WaitLeak implements Runnable
{
public
static Object LOCK = new Object();
public static void main(String[] args)
throws Exception
{
int WAITTIME = Integer.parseInt(args[0]);
int NUMTHREADS = 4;
(new Thread(new WaitLeakNotifier(WAITTIME))).start();
for (int i = 0; i < NUMTHREADS; i++)
{
Thread.sleep(1000);
(new Thread(new WaitLeak())).start();
}
}
public void run()
{
System.out.println("Starting thread " + Thread.currentThread());
synchronized(LOCK)
{
try{
LOCK.wait();
} catch(InterruptedException e) {}
}
System.out.println("Terminating thread " + Thread.currentThread());
}
}
class WaitLeakNotifier implements Runnable
{
long waittime;
public WaitLeakNotifier(long time)
{
waittime = time;
}
public void run()
{
long now = System.currentTimeMillis();
long diff = 0;
while( (diff = System.currentTimeMillis() - now) < waittime)
{
try {
Thread.sleep(waittime - diff);
} catch(InterruptedException e){}
}
synchronized(WaitLeak.LOCK)
{
WaitLeak.LOCK.notifyAll();
}
}
}
实现竟态条件
图 1 显示了三种可能的场景,在通知发送之前,它们所用的延迟不同。
顶部的面板显示了延迟相对较大(例如 10 秒)的程序,如下所示:
java WaitLeak 10000
这种方式造成所有 4 个 WaitLeak 线程均启动、等待、在 10 秒钟后得到通知,然后终止。
图 1 的第 2 个面板显示的程序,它的延迟为 WaitLeak 启动一半的时候,如 2 或 3 秒:
java WaitLeak 2000
在这个场景中,比通知线程启动得早的 WaitLeak 线程得到通知并终止,但是在通知发送之后启动的 WaitLeak 线程会一直等下去。
第 3 个场景的延迟时间非常短(例如 1 毫秒),如下所示,效果如图 1 的第 3 个面板所示。
java WaitLeak 1
图 1. 等待泄漏实战
在这个例子中, WaitLeakNotifier 在其他线程启动之前发送通知。所以没有线程会从 WaitLeakNotifier 得到通知,从而造成所有线程都一直阻塞在等待状态。
清单 2 显示了启动几分钟后的堆栈跟踪,截取自第 2 个场景。(可以在 Windows 上按 Ctrl+Break 得到堆栈跟踪,然后在 Unix 上用 Ctrl+\,或者向进程发送 kill -3。)
清单 2. java WaitLeak 2000 的线程堆栈转储
"Thread-4" prio=5 tid=0x00a0eee8 nid=0xf04 in Object.wait() [2d1f000..2d1fd8c]
at java.lang.Object.wait(Native Method)
- waiting on <0x1002c780> (a java.lang.Object)
at java.lang.Object.wait(Unknown Source)
at WaitLeak.run(WaitLeak.java:25)
- locked <0x1002c780> (a java.lang.Object)
at java.lang.Thread.run(Unknown Source)
"Thread-3" prio=5 tid=0x00a0c418 nid=0xc5c in Object.wait() [2cdf000..2cdfd8c]
at java.lang.Object.wait(Native Method)
- waiting on <0x1002c780> (a java.lang.Object)
at java.lang.Object.wait(Unknown Source)
at WaitLeak.run(WaitLeak.java:25)
- locked <0x1002c780> (a java.lang.Object)
at java.lang.Thread.run(Unknown Source)
"Thread-2" prio=5 tid=0x00a0d7a0 nid=0x118c in Object.wait() [2c9f000..2c9fd8c]
at java.lang.Object.wait(Native Method)
- waiting on <0x1002c780> (a java.lang.Object)
at java.lang.Object.wait(Unknown Source)
at WaitLeak.run(WaitLeak.java:25)
- locked <0x1002c780> (a java.lang.Object)
at java.lang.Thread.run(Unknown Source)
发现等待泄漏
线程转储显示了等待泄漏的故障现象,但是重要的东西却从线程转储中漏掉了 —— 就是 没有 通知等待线程的那个线程。所以需要添加一些额外的上下文信息,以便协助识别出等待泄漏。通常,可能报告出两种故障模型 —— 死锁,或应用程序响应程度逐渐下降。
首先来考虑标准的死锁类型的问题报告:应用程序什么也不做(虽然对用户引发的事件可能仍然有响应)—— 应用程序部分或者完全冻结。等待泄漏的故障现象与普通的死锁报告类似,不同之处在于在堆栈转储中没有死锁的迹象。如果看到这种情况,就应当考虑可能是遇到了等待泄漏。
第 2 个场景是一个逐渐过载、响应越来越差的应用程序。在这个例子中,随着时间的推移,越来越多的线程进入等待泄漏状态,这意味着越来越多的线程(本来应当做事的)只是闲在那里,什么都不做。结果就是,应用程序被傻等着永远不会到来的通知的那些线程所阻塞。结果有些资源被耗尽 —— 可能是线程池用尽、或者是多过的线程导致内存不足错误、或者由于应用程序最终出现了与第一类死锁类型相同的故障现象的情况。这可能是一个比较容易诊断的等待泄漏,因为可以比较某一段时间内的堆栈转储,并查看某些特定的 Object.wait() 堆栈(可能在使用同一个锁)的数量是否一直持续增长。刚才看到的生产示例中有一个响应越来越慢的服务器,到最后,仅仅在几分种之后,43 个等待泄漏堆栈就变成了 108 个等待堆栈,很快服务器就不再响应任何请求。
结束语
有趣的是,我们并不相信有什么可以自动发现等待泄漏的方法,除非只有等待泄漏线程被遗忘(就像本文中的模拟情况,但是在真正的应用程序中很少有同类情况)。实际上,多数情况下很难确定哪个应该调用 notify() 的代码绝对不会因为被锁定的监视器而再次执行。所以手工检查可能是我们能做的最好方式 —— 而这正是本文的目标,在您的性能调优武器库中加上另一个工具。如果您偶然碰到等待泄漏,我们敢保证您早晚会认出它来,我们希望本文能尽早给您带来帮助。
转自http://www.ibm.com/developerworks/cn/java/j-perf01215/
分享到:
相关推荐
通过实战练习,你可以更好地掌握性能调优技巧。 除此之外,重构也是性能优化的一个重要环节。重构是为了改善代码的结构和可读性,而不改变其外在行为。良好的代码结构可以使得后续的优化更容易进行,同时也能避免...
性能优化是IT领域中至关重要的一个环节,尤其是在Java编程中,因为高效的代码执行能显著提升应用程序的用户体验,...在Java开发中,通过理解和应用上述策略,我们可以不断提升应用的运行效率,为用户提供更好的体验。
### 性能测试之内存泄露篇 #### 一、概念 在进行性能测试时,内存泄露是一个非常重要的问题。本文将详细介绍内存泄露及其与内存溢出的区别,并介绍如何监测和解决这些问题。 **内存泄露**指的是应用程序在运行...
通过对内存泄漏的基本概念、表现形式、影响及症状等方面的学习,我们可以更好地理解内存泄漏的本质及其处理方法。同时,结合实际案例分析,能够更深入地认识到内存管理的重要性以及如何有效地避免和解决内存泄漏问题...
为了更好地理解和分析JVM行为,开发者通常会借助各种工具,如VisualVM、JProfiler和JConsole等。这些工具可以实时监控JVM的内存状态、CPU使用率、线程情况,帮助定位性能瓶颈。 总的来说,深入理解Java虚拟机对于...
【性能学习方法】 性能测试是软件开发过程中至关重要的一环,其目标是通过模拟真实...通过深入理解这些知识点,开发者可以更好地预防和解决性能问题,确保系统的长期稳定运行,从而满足业务需求,延长系统的使用寿命。
并发测试关注系统在多个用户同时操作时的行为,检查是否存在竞态条件、死锁等并发问题。 九、性能调优 通过性能测试发现的问题,需要进行代码优化、数据库调整、服务器配置修改等,以提高系统性能。 十、性能测试...
这有助于开发者更好地编写多线程程序,并优化应用程序的性能。 ##### 2. **内核资源管理** - **原文**: “IPC资源是持久的:它们必须被创建者进程、当前所有者或超级用户进程显式地取消分配。” - **解析**: ...
标题和描述所指的知识点围绕着如何在C++中进行高效编程,特别关注于内存管理和性能优化。在这方面的知识是针对那些对C++有基本理解,并希望通过优化自己的代码来提高效率和性能的开发者。 首先,C++是一种支持多...
JProfiler是一款强大的Java性能分析工具,它提供了丰富的功能来帮助开发者深入理解应用程序的内存管理和性能状况。通过JProfiler,你可以实时监测应用的内存使用、CPU消耗、线程状态,以及查找可能存在的内存泄漏...
内存泄露是计算机科学中一个非常重要的概念,尤其是在iOS和Android等移动应用开发中。...通过学习和分析这些内容,开发者可以更好地理解内存泄露的问题,并掌握如何在实际项目中预防和处理内存泄露。
《Eclipse性能优化——<深度理解JVM>读书笔记》主要涵盖了如何利用Eclipse IDE进行Java应用程序的性能优化,以及深入理解JVM的...同时,不断学习和实践,深入理解这些底层机制,将有助于我们更好地应对复杂的开发挑战。
在Linux中,可以使用pthread库创建和管理线程,同时要关注死锁、竞态条件和资源饥饿等并发问题。 2. **非阻塞I/O与异步I/O**:Linux提供了epoll机制,用于高效地处理大量并发连接。epoll支持水平触发和边缘触发两种...
在实际测试过程中,我们还需要使用专业的性能测试工具,例如JMeter、LoadRunner、 Gatling等,它们可以帮助我们创建复杂的测试场景,收集和分析测试数据,以便更好地理解和改进系统性能。 最后,性能测试的流程通常...
本书深入探讨了如何在C++编程中高效地使用内存资源,以及如何进行性能优化,以编写出既高效又优雅的代码。 书籍作者Rene Alexander和Graham Bensley均拥有十几年的软件开发经验,他们曾为多个不同的公司和项目提供...
源码在游戏性能分析中扮演着关键角色,因为只有通过查看和理解源代码,开发者才能精确地找到性能问题的根源。源码分析工具通常提供诸如代码剖析、函数调用堆栈、内存分配追踪等功能,帮助开发者定位代码中可能导致...
性能测试是软件开发不可或缺的一部分,通过对软件进行全面的性能测试,可以帮助开发者更好地了解软件的实际性能表现,及时发现并解决潜在的性能问题,从而提升软件的整体质量和用户体验。希望本文能够为读者提供有益...