技术开发 频道

J2EE中的死锁问题


【IT168 技术文档】 
    
     大多数重要的应用程序都涉及高度并发性和多个抽象层。并发性与资源争用有关,并且是导致死锁问题增多的因素之一。多个抽象层使隔离并修复死锁环境的工作变得更加困难。

  通常,当同时执行两个或两个以上的线程时,如果每个线程都占有一个资源并请求另一个资源,这时就会出现死锁情况。因为如果一个线程不能获取资源,则所有线程都不能继续执行,我们称那个特定的线程被阻塞;如果每个线程都由于同组中另一个线程所占有的资源而被阻塞,我们就称这个线程组被死锁。

  在本文中,我们将讨论发生在典型的重要J2EE应用程序中的两大类死锁情况:“简单”数据库死锁和跨资源死锁。虽然我们的讨论基于J2EE平台,但也适用于其他技术平台。 

     数据库死锁

  在数据库中,如果一个连接占用了另一个连接所需的数据库锁,则它可以阻塞另一个连接。如果两个或两个以上的连接相互阻塞,则它们都不能继续执行,这种情况称为死锁。

  数据库死锁问题不易处理,这是因为涉及到的锁定通常不是显式的。通常,对数据行进行隐式更新时,需要锁定该数据行,执行更新,然后在提交或回滚封闭事务时释放锁。由于数据库平台、配置的隔离级以及查询提示的不同,获取的锁可能是细粒度或粗粒度的,它会阻塞(或不阻塞)其他对同一数据行、表或数据库的查询。

  获取的锁依赖于内部生成的查询计划。当数据大小和分步随时间发生变化时,该计划也可能改变。这样在一个环境中获取一组锁的查询可以尝试在另一个环境中获取一组完全不同的锁。必要时,数据库可以随意地增加它的锁。例如,数据库可能会选择锁定整页,而不是锁定同一数据页中的10个数据行,这会阻塞对无需锁定的数据行的读写权限。

  基于数据库模式,读写操作会要求遍历或更新多个索引、验证约束、执行触发器等。每个要求都会引入更多锁。此外,其他应用程序还可能正在访问同一数据库模式中的某些对象,并获取不同于您的应用程序所具有的锁。

  所有这些因素综合在一起,数据库死锁几乎不可能被消除了。值得庆幸的是,数据库死锁通常是可恢复的:当数据库发现死锁时,它会强制销毁一个连接(通常是使用最少的连接),并回滚其事务。这将释放所有与已经结束的事务相关联的锁,至少允许其他连接中有一个可以获取它们正在被阻塞的锁。

  由于数据库具有这种典型的死锁处理行为,所以当出现数据库死锁问题时,数据库常常只能重试整个事务。当数据库连接被销毁时,会抛出可被应用程序捕获的异常,并标识为数据库死锁情况。如果允许死锁异常传播到初始化该事务的代码层之外,则该代码层可以只启动一个新事务并重做先前所有工作。要正确使用此策略,则在事务成功提交之前,它的代码不能有其他操作。注意:要限制重试次数,否则易导致死锁的代码块会永久循环下去。

  如果出现问题就重试,这种方法有点笨。但是,由于数据库可以自由地获取锁,所以几乎不可能保证两个或两个以上的线程不发生数据库死锁。此方法至少能保证在出现某些罕见的数据库死锁情况时,应用程序能正常运行。这比要求用户去重试操作要好得多。

  在J2EE应用程序中,开发人员可以设置一个EJB调用以使用Bean托管事务(BMT)——开发人员启动、提交或回滚特定的事务或容器托管事务(CMT)——调用方法前启动事务,并在方法完成后提交或回滚事务。如果EJB供应商提供retry-on-deadlock参数,从而可以通过容器托管事务自动完成此操作,那当然再好不过了。如果没有这种自动功能,开发人员最终将仅为了对死锁进行重试而强制EJB调用使用Bean托管事务。有关此操作的细节,请参见www.onjava.com/pub/a/onjava/2005/07/20/transactions.html

  遇到死锁问题和锁定其他线程的锁的具体频率在很大程度上取决于数据库平台、硬件、数据库模式和查询。在使用基于锁的并发控制的数据库(如MSSQL)中,未提交的写操作会阻止读操作,而未提交的读操作会阻止写操作,使数据库更易出现死锁问题。在多版本并发控制(MVCC)数据库(如Oracle)中,未提交的写操作不阻止读操作——读操作仅查看旧版本数据行。这虽然会引入其他问题,但不会造成同样多的死锁机会。我们要让自己熟悉这些数据库锁定模式,并注意自己正在使用的类型。

  在查找、修复以及避免数据库死锁方面,有一些很好的参考方法,但它们都不能彻底消除死锁的可能性。 

     跨资源死锁

  当死锁情况不完全局限于数据库时,将更难找到它。数据库对占有和请求的锁有识别能力,所以能检测整个数据库中的死锁;此外,数据库事务在确定哪些东西是原子、哪些不是方面提供了一个良好的界线,所以能轻松地回滚事务,使其从死锁中恢复。其他环境(如Java虚拟机)中的死锁或可跨环境的死锁更加危险,因为环境不能(或没有)检测到这些死锁并尝试恢复。更糟糕的是,这些死锁会产生综合效果——如果两个线程占有某些资源集时出现死锁,则其他任何尝试访问其中一个资源的线程也将被阻塞,该线程已经获取的所有资源也被阻塞。这些死锁常常不易发现,但对常见模式有一定的了解将有助于识别和修复死锁问题。

  当环境中出现可疑的死锁情况时,您就需要考虑一些问题了。这些问题的答案将说明您正在处理的情形是下列情形中的哪一种(如果有的话),并提供了修复以下问题的详细信息。要考虑的一些重要事项包括: 
     涉及什么线程,它们的调用堆栈是什么?这需要进行一些详细的分析,将实际的死锁线程从那些只是被死锁的线程阻塞了的线程中分离出来。 
     这种死锁情况总是在特定的代码路径中出现(每次执行这些特定的操作时),还是依赖于两个或两个以上同时执行的代码路径呢? 
     涉及的数据库连接是什么?每个连接占有的数据库锁是什么?每个连接尝试获取的数据库锁是什么?每个数据库连接响应的Java虚拟机线程是什么?

  下一小节介绍了三种常见的发生跨资源死锁的情形。 

     跨资源死锁情形之1:客户端的增加导致资源池耗尽

  我们要介绍的第一种死锁情形是单纯由于负载而造成的,即资源池太小,而每个线程需要的资源超过了池中的可用资源。例如,考虑一个使用数据库连接的EJB调用,执行一个嵌套的EJB调用(使用同一连接池中不同的数据库连接)。例如,如果该嵌套的EJB调用声明为RequiresNew,就会出现死锁情形。

  在正常负载或者有足够大小的连接池的情况下,EJB调用将从池中获取一个数据库连接,然后调用嵌套的EJB。嵌套的EJB调用将从池中获取另一个数据库连接,提交内部事务,然后向池返回连接。外部EJB调用将提交自己的事务,并向池返回其连接。

  但是,假设连接池最多有10个连接,同时有10个对外部EJB的并发调用。这些线程中每一个都需要一个数据库连接用来清空池。现在,每个线程都执行嵌套的EJB调用(需要获取第二个数据库连接)。则所有线程都不能继续,但又都不放弃自己的第一个数据库连接。这样,10个线程都将被死锁。

  如果研究此类死锁情形,会发现线程转储中有大量等待获取资源的线程,以及同等数量的空闲且未阻塞的活动数据库连接。当应用程序死锁时,如果可以在运行时检测连接池,应该能确认连接池实际上已空。

  修复此类死锁的方法包括:增加连接池的大小或者重构代码,以便单个线程不需要同时使用很多数据库连接。如果单线程需要的最大数据库连接数为M,且可能的最大并发调用数为N,则要避免此问题,在池中所需的最小连接数为(N*(M01))+1。或者可以设置内部EJB调用以使用不同的连接池,即使外部调用的连接池为空,内部调用也能使用自己的连接池继续。

  跨资源死锁情形之2:单线程、多冲突数据库连接

  对同一线程执行嵌套的EJB调用时还会出现第二种跨资源死锁情形,此情形即使在非高负载系统中通常也会发生。同上面的示例一样,两个EJB调用使用不同的连接来连接到同一个数据库。因为只有嵌套调用完成后调用方才能继续,所以调用方的数据库连接实际上被嵌套调用的数据库连接阻塞了,虽然数据库没有注意到这种关系。如果第一个(外部)连接已获取第二个(内部)连接所需要的数据库锁,则第二个连接将永久阻塞第一个连接,并等待第一个连接被提交或回滚,这就出现了死锁情形。因为数据库没有注意到两个连接之间的关系,所以数据库不会将此情形检测为死锁。

  作为一个具体的示例,考虑一个数据加载EJB调用。此EJB调用获取一个大型对象,并在不同阶段中将其保存在数据库中。当它执行数据加载时,它会更新一个单独的表,以记录挂起数据加载操作的状态。我们希望状态更新立即可见,但不希望在未完成的状态下看到加载的数据,所以要通过调用“RequiresNew” EJB来完成。总的来说,这种不完善的数据加载方法如清单1中的代码所示。
  清单1
public void bulkLoadData(DataBatch batch) { int batchId = batch.getId(); // Since this executeUpdate call doesn誸 happen in a separate // transaction, it wouldn't be visible anyway, but the effect is // far worse: a cross-resource deadlock. executeUpdate("update batch_status set status='Started' " + "where batch_id=" + batchId); validateData(batch); updateBatchStatus(batchId, "Validated"); // RequiresNew EJB call loadDataStage1(batch); updateBatchStatus(batchId, "Stage 1 complete"); // RequiresNew EJB call loadDataStage2(batch); updateBatchStatus(batchId, "Stage 2 complete"); // RequiresNew EJB call finalizeDataLoad(batch); updateBatchStatus(batchId, "Complete"); // RequiresNew EJB call }



0
相关文章