大多数重要的应用程序都涉及高度并发性和多个抽象层。并发性与资源争用有关,并且是导致死锁问题增多的因素之一。多个抽象层使隔离并修复死锁环境的工作变得更加困难。
通常,当同时执行两个或两个以上的线程时,假如每个线程都占有一个资源并请求另一个资源,这时就会出现死锁情况。因为假如一个线程不能获取资源,则所有线程都不能继续执行,我们称那个特定的线程被阻塞;假如每个线程都由于同组中另一个线程所占有的资源而被阻塞,我们就称这个线程组被死锁。
在本文中,我们将讨论发生在典型的重要J2EE应用程序中的两大类死锁情况:“简单”数据库死锁和跨资源死锁。虽然我们的讨论基于J2EE平台,但也适用于其他技术平台。
数据库死锁
在数据库中,假如一个连接占用了另一个连接所需的数据库锁,则它可以阻塞另一个连接。假如两个或两个以上的连接相互阻塞,则它们都不能继续执行,这种情况称为死锁。
数据库死锁问题不易处理,这是因为涉及到的锁定通常不是显式的。通常,对数据行进行隐式更新时,需要锁定该数据行,执行更新,然后在提交或回滚封闭事务时释放锁。由于数据库平台、配置的隔离级以及查询提示的不同,获取的锁可能是细粒度或粗粒度的,它会阻塞(或不阻塞)其他对同一数据行、表或数据库的查询。
获取的锁依靠于内部生成的查询计划。当数据大小和分步随时间发生变化时,该计划也可能改变。这样在一个环境中获取一组锁的查询可以尝试在另一个环境中获取一组完全不同的锁。必要时,数据库可以随意地增加它的锁。例如,数据库可能会选择锁定整页,而不是锁定同一数据页中的10个数据行,这会阻塞对无需锁定的数据行的读写权限。
基于数据库模式,读写操作会要求遍历或更新多个索引、验证约束、执行触发器等。每个要求都会引入更多锁。此外,其他应用程序还可能正在访问同一数据库模式中的某些对象,并获取不同于您的应用程序所具有的锁。
所有这些因素综合在一起,数据库死锁几乎不可能被消除了。值得庆幸的是,数据库死锁通常是可恢复的:当数据库发现死锁时,它会强制销毁一个连接(通常是使用最少的连接),并回滚其事务。这将释放所有与已经结束的事务相关联的锁,至少答应其他连接中有一个可以获取它们正在被阻塞的锁。
由于数据库具有这种典型的死锁处理行为,所以当出现数据库死锁问题时,数据库经常只能重试整个事务。当数据库连接被销毁时,会抛出可被应用程序捕捉的异常,并标识为数据库死锁情况。假如答应死锁异常传播到初始化该事务的代码层之外,则该代码层可以只启动一个新事务并重做先前所有工作。要正确使用此策略,则在事务成功提交之前,它的代码不能有其他操作。注重:要限制重试次数,否则易导致死锁的代码块会永久循环下去。
假如出现问题就重试,这种方法有点笨。但是,由于数据库可以自由地获取锁,所以几乎不可能保证两个或两个以上的线程不发生数据库死锁。此方法至少能保证在出现某些罕见的数据库死锁情况时,应用程序能正常运行。这比要求用户去重试操作要好得多。
在J2EE应用程序中,开发人员可以设置一个EJB调用以使用Bean托管事务(BMT)——开发人员启动、提交或回滚特定的事务或容器托管事务(CMT)——调用方法前启动事务,并在方法完成后提交或回滚事务。假如EJB供给商提供retry-on-deadlock参数,从而可以通过容器托管事务自动完成此操作,那当然再好不过了。假如没有这种自动功能,开发人员最终将仅为了对死锁进行重试而强制EJB调用使用Bean托管事务。
碰到死锁问题和锁定其他线程的锁的具体频率在很大程度上取决于数据库平台、硬件、数据库模式和查询。在使用基于锁的并发控制的数据库(如MSSQL)中,未提交的写操作会阻止读操作,而未提交的读操作会阻止写操作,使数据库更易出现死锁问题。在多版本并发控制(MVCC)数据库(如Oracle)中,未提交的写操作不阻止读操作——读操作仅查看旧版本数据行。这虽然会引入其他问题,但不会造成同样多的死锁机会。我们要让自己熟悉这些数据库锁定模式,并注重自己正在使用的类型。
在查找、修复以及避免数据库死锁方面,有一些很好的参考方法,但它们都不能彻底消除死锁的可能性。
跨资源死锁
当死锁情况不完全局限于数据库时,将更难找到它。数据库对占有和请求的锁有识别能力,所以能检测整个数据库中的死锁;此外,数据库事务在确定哪些东西是原子、哪些不是方面提供了一个良好的界线,所以能轻松地回滚事务,使其从死锁中恢复。其他环境(如Java虚拟机)中的死锁或可跨环境的死锁更加危险,因为环境不能(或没有)检测到这些死锁并尝试恢复。更糟糕的是,这些死锁会产生综合效果——假如两个线程占有某些资源集时出现死锁,则其他任何尝试访问其中一个资源的线程也将被阻塞,该线程已经获取的所有资源也被阻塞。这些死锁经常不易发现,但对常见模式有一定的了解将有助于识别和修复死锁问题。
当环境中出现可疑的死锁情况时,您就需要考虑一些问题了。这些问题的答案将说明您正在处理的情形是下列情形中的哪一种(假如有的话),并提供了修复以下问题的具体信息。要考虑的一些重要事项包括:
涉及什么线程,它们的调用堆栈是什么?这需要进行一些具体的分析,将实际的死锁线程从那些只是被死锁的线程阻塞了的线程中分离出来。
这种死锁情况总是在特定的代码路径中出现(每次执行这些特定的操作时),还是依靠于两个或两个以上同时执行的代码路径呢?
涉及的数据库连接是什么?每个连接占有的数据库锁是什么?每个连接尝试获取的数据库锁是什么?每个数据库连接响应的Java虚拟机线程是什么? 下一小节介绍了三种常见的发生跨资源死锁的情形。
三层交换技术