据我了解,Guava的缓存默认锁定密钥。因此,如果线程t1和线程t2都试图获得相同的密钥,则只有一个实际上会加载它,而另一个线程等待第一个获取该值并在之后获得相同的值。
这是一个非常好的默认行为,但如果您正在处理彼此依赖的多个缓存,则它不是那么理想。
我们处于一种情况,我们有多个缓存实例和多个线程。线程查询多个缓存以完成其工作。因此,缓存实例彼此依赖。它实际上归结为以下场景:
线程t1
Value v1 = cache1.get(k, new Callable<Value>() {
Value call() {
//do something
Value v2 = cache2.get(k, doRealWorkCallable());
Value v = calculateFrom(v2)
return v;
}
});
线程t2
Value v2 = cache2.get(k, new Callable<Value>() {
Value call() {
//do something
Value v1 = cache1.get(k, doRealWorkCallable());
Value v = calculateFrom(v1)
return v;
}
});
如果我正确理解锁定策略,上述情况可能会导致死锁情况: 线程t1保持对cache1中的k的锁定,等待cache2中的k锁定。 线程t2保持对cache2中k的锁定,等待cache1中的k锁。
番石榴有没有办法防止这种僵局?正如我所看到的,只要您使用CacheLoader
或Callable
,就可能会陷入死锁状态,因为两者都会锁定正在加载的密钥。
我认为我们可以使用旧的“检查是否存在于缓存中,如果不存在:计算并将其放在那里”:
Value v1 = cache1.getIfPresent(k);
if (v1 == null) {
//get it using cache2
Value v2 = cache2.getIfPresent(k);
if (v2 == null) {
v2 = doRealWork();
cache2.put(k,v2);
}
v1 = calculateFrom(v2);
cache1.put(v1);
}
(当然第二个线程的另一种方式)
这带来了价值的“可能不需要”的计算成本,这样做不会给死锁线程带来风险。
使用番石榴有没有“更好”的方法呢?
我们正在从一个我们无法控制的外部系统调用多个Web服务。这些Web服务提供的数据是分层的,并通过引用链接。例如:
class WSOrganization {
Integer id;
String name;
List<Integer> employeeIds; //like a collection of foreign keys
}
class WSEmployee {
Integer id;
String name;
Integer organizationId; //like a foreignkey
}
有些地方我们需要员工和我们需要组织的地方。如果我们要求组织,我们会热切地这样做。如果我们取得员工,我们也需要组织。代码分布在多个服务存根之间,依此类推,但最终归结为:
//in EJB 1
PrefetchedOrganization getOrganization(Integer orgId) {
WSOrganization org = orgService.getOrganizationById(orgId);
for (Integer employeeId : org.employeeIds) {
WSEmployee employee = employeeService.getEmployeeById(employeeId);
listOfEmployees.add(employee);
}
return createPrefetchedOrgWithEmployees(org, listOfEmployees);
}
//in EJB 2
PrefetchedEmployee getEmployee(Integer employeeId) {
WSEmployee employee = employeeService.getEmployeeById(employeeId);
PrefetchedOrganization orgOfEmployee = ejb2.getOrganization(employee.organisationId);
return orgOfEmployee.employee(employeeId);
}
现在,我们想在EJB 1和EJB 2上使用javax.interceptor.Interceptor
来介绍缓存。
@AroundInvoke
public Object aroundInvoke(InvocationContext invocation) {
Object object = getElementFromCache(invocation);
return object;
}
可能会发生两个线程以相反的顺序调用这两个方法,我们绝对不希望它们相互阻塞。
Integer id = idFrom(invocation);
if (cache.containsKey(id)) {
return cache.get(id);
} else {
Object result = invocation.proceed();
cache.put(id, result);
return result;
}
答案 0 :(得分:0)
在执行Callable for Cache时,Guava似乎没有锁定密钥。否则,您提供的代码将始终死锁,例如:
getOrganization(1337):
(contains employee X)
getEmployee(x):
getOrganization(1337) // deadlock by recursion!!!
此方法提供了传统“if cached,return;否则create,cache和return”模式的简单替代。
请注意,我没有尝试过这个,但只是从文档中看起来Guava似乎在多次运行Callable时犯了错误。
简而言之,这不是问题!