2

据我了解,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。

番石榴有什么办法可以防止这种死锁情况?在我看来,只要您使用CacheLoaderor 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;
}
4

1 回答 1

0

在执行缓存的 Callable 时,Guava 似乎没有锁定键。否则您提供的代码将始终死锁,例如:

getOrganization(1337):
    (contains employee X)
    getEmployee(x):
        getOrganization(1337) // deadlock by recursion!!!

Cache.get的文档指出:

此方法提供了对传统“如果缓存,则返回;否则创建,缓存并返回”模式的简单替代。

请注意,我还没有尝试过,但从文档来看,Guava 似乎在多次运行 Callable 方面犯了错误。

简而言之,这不是问题!

于 2014-02-14T18:45:59.913 回答