J2EE中的死锁问题
在上面的示例中,使用updateBatchStatus方法执行“RequiresNew” EJB调用实际上可以更新batch_status数据库表,即使没有看到当前事务的效果,也能立即看到状态的改变。对executeUpdate的调用不是EJB调用,所以它和bulkLoadData的其他部分在同一个事务中执行。
如上所述,即使不存在并发,此代码也将导致死锁。当bulkLoadData调用executeUpdate方法时,它更新现有的数据库行,这涉及为该行获取写锁。对updateBatchStatus的嵌套EJB调用将在单独的数据库连接上执行,并尝试执行一个非常相似的查询,但它将阻塞,因为不能获取必需的写锁。从数据库的角度来说,只要提交或回滚第一个连接的事务,第二个连接就可以继续。但是,Java虚拟机不允许在完成所有对updateBatchStatus的调用前完成bulkLoadD调用,这样就出现了死锁情形。
该示例表明,一个更新会阻塞另一个更新,所以它会在任何数据库中导致死锁。如果初始更新查询是一个简单的选择查询,那么该示例仅在使用基于锁的并发控制的数据库上导致死锁,在这种数据库中,一个连接的读锁可以阻止另一个连接获取写锁。不管在哪种情况下,此类死锁即不依赖于同步,也不依赖于负载,而且线程转储将显示一个等待数据库响应的Java线程,但该线程与两个有效的数据库连接相关联。在这些数据库连接中,有一个将处于空闲状态,但会阻塞其他连接。
此情形有多种具体的变种,可以涉及多个线程和两个以上的数据库连接。例如,外部EJB调用的数据库连接可能已经获取了数据库锁,该锁阻塞了另一个无关数据库连接的继续,但这个无关数据库连接已经获取了阻塞嵌套EJB调用的数据库操作的锁。这个特例是依赖于同步的,并将显示多个等待数据库响应的Java线程。其中至少有一个Java线程将与两个活动数据库连接相关联。
跨资源死锁情形之3:Java虚拟机锁与数据库锁相冲突
第三种死锁情形发生在数据库锁与Java虚拟机锁并存的时候。在这种情况下,一个线程占有一个数据库锁并尝试获取Java虚拟机锁(尝试进入同步的锁)。同时,另一个线程占有Java虚拟机锁并尝试获取数据库锁。再次地,数据库发现一个连接阻塞了另一个连接,但由于无法阻止连接继续,所以不会检测到死锁。Java虚拟机发现同步的锁中有一个线程,并有另一个尝试进入的线程,所以即使Java虚拟机能检测到死锁并对它们进行处理,它还是不会检测到这种情况。
为了说明此种死锁情形,我们以一个简单的(不完善的)read-through cache为例。该cache是数据库表中备份的HashMap。如果出现缓存命中,它就从HashMap返回一个值。但在缓存缺失的情况下,它将从数据库读取值,将其添加到HashMap,然后返回该值,如清单2所示。
清单 2
这是一个简单的遍历cache。注意:get()方法是同步的,这是因为我们访问了非线程安全容器,并要求containsKey/put组合在缓存缺失时是原子性的。public class SimpleCache ...{
private Map cache = new HashMap();
public synchronized Object get(String key) ...{
if (cache.containsKey(key)) ...{
return cache.get(key);
} else ...{
Object value = queryForValue(key);
cache.put(key, value);
return value;
}
}
private Object queryForValue(String key) ...{
return executeQuery("select value from cache_table " +
"where key='" + key + "'");
}
public synchronized void clearCache() ...{
cache.clear();
}
![]()
// other methods omitted for brevity
}
该cache相当简单易懂:它约定,如果更改支持缓存的表中的数据,则应调用clearCache(),这样缓存就可以避免处理陈旧的数据。产生的缓存缺失将相应地重新进入缓存。
我们现在来考虑可以更改此数据并清除缓存的代码:
上面的代码在简单的例子中能正常运行。但是,在使用基于锁的并发控制的数据库中,updateData中的查询将阻止queryForValue中的选择查询的执行,因为update语句将获取一个写锁,从而阻止选择查询获取同一数据行上的读锁。如果同步没有问题,一个线程可以尝试读取缓存中的给定值,并在另一个线程在数据库中更新该值时得到缓存缺失。如果数据库先执行update语句,它将阻塞select语句继续执行。但是,执行select语句的线程来自同步的get方法,所以它获取了SimpleCache上的锁。要返回updateData中的线程,它必须调用clearCache(),但不能获取锁(clearCache()是同步的)。public void updateData(String key, String value) ...{
executeUpdate("update cache_table set value='" + value +
"' where key='" + key + "'");
SimpleCache.getInstance().clearCache();
}
当处理此情形的实例时,将有一个等待数据库响应的Java线程和一个等待获取Java虚拟机锁的线程。每个线程将与一个数据库连接相关联,其中一个连接阻塞另一个连接。修复方法是占有Java虚拟机锁时避免执行数据库操作,可以重写leCache的get()方法,如下所示:
既然现在我们知道了会发生此死锁情况,就可以使用Thread.holdsLock()向queryForValue方法添加检查以尝试避免死锁情况:public Object get(String key) ...{
synchronized(this) ...{
if (cache.containsKey(key)) ...{
return cache.get(key);
}
}
Object value = queryForValue(key);
synchronized(this) ...{
cache.put(key, value);
}
return value;
}
上例中的Thread.holdsLock()很有用,但是只有在我们知道需要留心哪个锁时它才会发挥作用。如果有一个类似的方法可以确定当前线程占有哪个Java虚拟机锁,那么会很有用。任何执行任何种类的RPC调用、数据库访问等的代码片段都可以抛出异常或记录警告,指示在占有Java虚拟机锁时执行这些操作会有危险。private Object queryForValue(String key) ...{
assert(!Thread.holdsLock(this));
return executeQuery(...);
}
注意:虽然我们修复了上例中的死锁问题,但它仍有缺陷,因为在提交updateData的事务之前清空了缓存。如果在调用clearCache后、提交updateData事务前出现缓存缺失,则该缓存将加载旧数据,因为新数据尚未可见。这里的修复方法是仅在提交更改后清空缓存。注意,这只在MVCC数据库中发生。在基于锁的数据库中,挂起的update将阻塞缓存的读操作,所以在提交update的事务后缓存才能读取正确值。
经验法则
下面的这些指导可以帮您避免死锁问题,或者至少在出现死锁时能诊断并修复它们。
保持事务简短。
了解数据库锁行为(以及事务分离层)。
假定任何数据库访问都有可能陷入数据库死锁状况,但是能正确重试。
事务完成前不要更新任何非事务状态(内存状态、缓存等)。
确保在峰值并发时有足够大的资源池。
尝试不在同一时刻获取多个资源。如果必需,则按相同的顺序每次获取一个资源。
了解如何从应用服务器获取完整的线程转储以及从数据库获取数据库连接列表(包括互相阻塞的连接),知道每个数据库连接与哪个Java线程相关联。了解Java线程和数据库连接之间映射的最简单方法是向连接池访问模式添加日志记录功能。
当进行嵌套的EJB调用时,了解哪些调用使用与调用方同样的数据库连接。即使嵌套的调用运行在同一个全局事务中,它仍将使用不同的数据库连接,而这会导致跨资源死锁。
避免执行数据库调用和EJB调用,或在占有Java虚拟机锁时,执行其他与Java虚拟机无关的操作。如果有需要留心的特定Java虚拟机锁,就使用assert(!Thread.holdsLock(...)),从而避免以后的代码更改不会在无意间违背此规则。
结束语
J2EE应用程序中的跨资源死锁是一个大问题——它能导致整个应用程序慢慢终止,还很难被分离和修复,尤其是当开发人员不熟悉如何分析死锁环境的时候。我们讨论的情形将有助于您理解一些常见的死锁情形,并为您提供查找死锁的思路。更重要的是,我们概括的经验法则提供了一些要在代码中遵守的惯例,从而避免所有类似的死锁问题。
参考资料
使用Bean托管事务(BMT)时如何获取“RequiresNew”和“NotSupported”事务行为:
www.onjava.com/pub/a/onjava/2005/07/20/transactions.html
J2EE应用程序中的数据库死锁:www.theserverside.com/patterns/thread.tss?thread_id=14482
WebSphere文档中对情形1的讨论:http://publib.boulder.ibm.com/infocenter/wasinfo/v5r1/index.jsp?
topic=/com.ibm.Websphere.base.doc/info/aes/ae/cdat_conpool.html
针对跨EJB调用的连接共享的WebSphere相关讨论:www-
128.ibm.com/developerworks/Websphere/library/techarticles/0404_tang/0404_tang.html
0
相关文章
