コードは、veryCostlyOperation(name)を複数回呼び出す可能性があります。問題は、マップを検索した後に同期されていないステップがあることです。
public void request(String name) {
Resource resource = resources.get(name);
if (resource == null) {
synchronized(this) {
//...
}
}
//...
}
マップからのget()はマップによって同期されますが、結果のnullのチェックは何によっても保護されません。複数のスレッドが同じ「名前」を要求してこれに入ると、実際にcostlyOperationを終了してリソースをリソースマップに配置するまで、すべてのスレッドにresources.get()からのnull結果が表示されます。
より単純で機能しますが、スケーラブルではないアプローチは、法線マップを使用して、要求メソッド全体を同期させることです。実際に問題が発生しない限り、私は単純なアプローチを選択します。
スケーラビリティを高めるために、同期後(this)にマップを再度チェックしてコードを修正し、上記のケースをキャッチできます。同期(this)では1つのスレッドのみがcostlyOperationを実行できるため、それでも最高のスケーラビリティは得られませんが、多くの実際のケースでは、異なるリソースへの同時リクエストを許可しながら、同じリソースに対する複数の実行を防止したいだけです。その場合、要求されているリソースで同期するための機能が必要です。非常に基本的な例:
private static class ResourceEntry {
public Resource resource;
}
private Map<String, ResourceEntry> resources = new HashMap<String, ResourceEntry>();
public Resource request(String name) {
ResourceEntry entry;
synchronized (resources) {
entry = resources.get(name);
if (entry == null) {
// if no entry exists, allocate one and add it to map
entry = new ResourceEntry();
resources.put(name, entry);
}
}
// at this point we have a ResourceEntry, but it *may* be no loaded yet
synchronized (entry) {
Resource resource = entry.resource;
if (resource == null) {
// must create the resource
resource = costlyOperation(name);
entry.resource = resource;
}
return resource;
}
}
これは大まかなスケッチにすぎません。基本的に、ResourceEntryの同期ルックアップを作成し、次にResourceEntryで同期して、特定のリソースが1回だけビルドされるようにします。