如何确保多个番石榴缓存不会相互锁定?

时间:2013-08-19 07:46:20

标签: java caching concurrency locking guava

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

番石榴有没有办法防止这种僵局?正如我所看到的,只要您使用CacheLoaderCallable,就可能会陷入死锁状态,因为两者都会锁定正在加载的密钥。

我认为我们可以使用旧的“检查是否存在于缓存中,如果不存在:计算并将其放在那里”:

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;
}

可能会发生两个线程以相反的顺序调用这两个方法,我们绝对不希望它们相互阻塞。

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;
}

1 个答案:

答案 0 :(得分:0)

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

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

documentation for Cache.get州:

  

此方法提供了传统“if cached,return;否则create,cache和return”模式的简单替代。

请注意,我没有尝试过这个,但只是从文档中看起来Guava似乎在多次运行Callable时犯了错误。

简而言之,这不是问题!