据我了解,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
or 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);
}
现在,我们想在这里通过javax.interceptor.Interceptor
在 EJB 1 和 EJB 2 上使用来介绍缓存。
@AroundInvoke
public Object aroundInvoke(InvocationContext invocation) {
Object object = getElementFromCache(invocation);
return object;
}
可能会发生两个线程以相反的顺序调用这两个方法,我们绝对不希望它们相互阻塞。
EDIT2:使用 Hashmaps 的 getElementFromCache() 示例实现
Integer id = idFrom(invocation);
if (cache.containsKey(id)) {
return cache.get(id);
} else {
Object result = invocation.proceed();
cache.put(id, result);
return result;
}