7

我有一個Web應用程序,人們要求資源。這些資源使用同步哈希映射來提高效率。這裏的問題是當兩個不同的請求同時出現在相同的未緩存資源中時:檢索資源的操作佔用大量內存,所以我想避免爲同一資源多次調用它。在HashMap緩存中同步

有人可以告訴我,以下代碼段是否存在任何潛在問題?提前致謝。

private Map<String, Resource> resources = Collections.synchronizedMap(new HashMap<String, Resource>()); 

public void request(String name) { 

    Resource resource = resources.get(name); 

    if (resource == null) { 
    synchronized(this) { 
     if (resources.get(name) == null) { 
     resource = veryCostlyOperation(name); // This should only be invoked once per resource... 
     resources.put(resource); 
     } else { 
     resource = resources.get(name); 
     } 
    } 
    } 

    ... 

} 

回答

6

一個可能的問題是,你被​​塊內部執行veryCostlyOperation()造成不必要的競爭,使許多線程不能在同一時間找回自己(獨立的)資源。這可以通過使用Future<Resource>作爲地圖的值來解決:

Map<String, Future<Resource>> map = new ConcurrentHashMap<String, Future<Resource>>();  
... 
Future<Resource> r = map.get(name); 
if (r == null) { 
    FutureTask task = null; 
    synchronized (lock) { 
     r = map.get(name); 
     if (r == null) { 
      task = new FutureTask(new Callable<Resource>() { 
       public Resource call() { 
        return veryCostlyOperation(name); 
       } 
      }); 
      r = task; 
      map.put(name, r); 
     } 
    } 
    if (task != null) task.run(); // Retrieve the resource 
} 

return r.get(); // Wait while other thread is retrieving the resource if necessary 
+0

感謝您使用FutureTask,我已經在內部看過這個類,但從來不知道它有這個特性。 – Boris 2011-03-30 16:06:59

+3

與此相關的問題是您可以同時在多個資源上調用veryCostlyOperation。 OP提到他不想在同一個資源上稱兩次,但這可能是曲線擬合的評論。如果在您的代碼中同時請求十二個不同的資源,那麼十二個「veryCostlyOperation」調用將全部並行。如果他們的確內存密集,你可能會用完內存。 – 2011-03-30 16:14:12

1

我看到的唯一潛在問題是您同步到this。如果同一類中的任何其他代碼也同步到this,則只有其中一個塊會立即運行。也許沒有其他事情做到這一點,那很好。不過,我總是擔心下一位程序員要做什麼。 (或者我在三個月內忘記了這段代碼)

我會建議創建一個通用的同步對象,然後同步到那個。

 
private final Object resourceCreationSynchObject = new Object(); 

然後

 
synchronized(this.resourceCreationSynchObject) { 
    ... 
} 

否則,這不正是你問什麼。它確保veryCostlyOperation不能並行調用。

另外,在​​區塊內重新獲取資源是非常好的想法。這是必要的,外面的第一次調用確保在資源已經可用時不會同步。但沒有理由第三次稱呼它。在​​塊內的第一件事,再將resource設置爲resources.get(name),然後檢查該變量是否爲空。這會阻止您在else條款內再次致電get

+0

爲什麼不使用資源作爲同步對象? – JenEriC 2011-03-30 15:21:40

+0

您可以,使用已經同步的對象作爲同步對象,這似乎讓我感到困惑。雖然沒有理由你不能。 – 2011-03-30 16:09:15

1

您的代碼看起來不錯,除非你正在同步比實際需要更多:

  • 使用ConcurrentHashMap,而不是同步HashMap將允許get方法的多次調用沒有鎖定。

  • 同步this而不是resources可能不是必需的,但它取決於您的其他代碼。

+0

如果他使用ConcurrentHashMap,get方法不同步,所以這不成問題。原始代碼已經可以防止veryCostlyOperation被多次調用同名,並且用ConcurrentHashMap替換同步的HashMap不會改變它。 – jarnbjo 2011-03-30 16:11:02

+0

好吧,如果你保持synchronizedCostlyOperation同步塊,你的權利。但是這正是我想強調的,因爲你確實提到同步「可能不是必需的」。正是出於這個原因。 – Boris 2011-04-01 07:47:59

0

您的代碼將可能調用veryCostlyOperation(名稱)多次。問題是,有仰視圖後不同步的步驟:)從地圖

public void request(String name) { 
    Resource resource = resources.get(name); 
    if (resource == null) { 
     synchronized(this) { 
      //... 
     } 
    } 
    //... 
} 

的get(是由地圖同步,但檢查空的結果不受任何保護。如果多個線程輸入請求相同的「名稱」,則所有這些線程都將從resources.get()中看到空結果,直到實際完成costlyOperation並將資源放入資源映射中。

一個更簡單和可行的方法,但可擴展性較差的方法是使用正常映射並使整個請求方法同步。除非在實踐中真的發現問題,否則我會選擇簡單的方法。

爲了獲得更高的可擴展性,您可以通過檢查地圖再次修復代碼,以便在捕獲上述情況後同步(此)。它仍然不能提供最好的可伸縮性,因爲同步(this)只允許一個線程執行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上同步以確保特定資源僅被建立一次