私の理解では、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 はキャッシュ 1 で k のロックを保持し、キャッシュ 2 でロック k を待機します。スレッド t2 は、キャッシュ 2 で k のロックを保持し、キャッシュ 1 でロック 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);
}
(そしてもちろん、2番目のスレッドでは逆です)
これには、デッドロックされたスレッドの危険を冒さないという利点がありますが、値の「おそらく不要な」計算のコストが伴います。
グアバでこれを行う「より良い」方法はありますか?
編集:以下の具体例
制御できない外部システムから複数の 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;
}
2 つのスレッドが 2 つのメソッドを逆の順序で呼び出す場合があり、それらが互いにブロックすることは絶対に望ましくありません。
EDIT2: ハッシュマップを使用した getElementFromCache() の実装例
Integer id = idFrom(invocation);
if (cache.containsKey(id)) {
return cache.get(id);
} else {
Object result = invocation.proceed();
cache.put(id, result);
return result;
}