2013-05-29 64 views
6

我正在處理一些第三方庫代碼,這些代碼涉及創建昂貴的對象並將其緩存在Map中。現有的實現是一樣的東西按鍵封鎖Java中的地圖

lock.lock() 
try { 
    Foo result = cache.get(key); 
    if (result == null) { 
     result = createFooExpensively(key); 
     cache.put(key, result); 
    } 
    return result; 
} finally { 
    lock.unlock(); 
} 

顯然,這不是最好的設計時Foos針對不同keys可以獨立創建。

我現在的黑客是使用MapFutures的:

lock.lock(); 
Future<Foo> future; 
try { 
    future = allFutures.get(key); 
    if (future == null) { 
     future = executorService.submit(new Callable<Foo>() { 
      public Foo call() { 
       return createFooExpensively(key); 
      } 
     }); 
     allFutures.put(key, future); 
    } 
} finally { 
    lock.unlock(); 
} 

try { 
    return future.get(); 
} catch (InterruptedException e) { 
    throw new MyRuntimeException(e); 
} catch (ExecutionException e) { 
    throw new MyRuntimeException(e); 
} 

但這似乎......有點哈克,原因有二:

  1. 的工作是在任意合併完成線。我很樂意在第一個線程上完成 這個嘗試獲取這個特定密鑰的線程,特別是因爲 它會被阻止。
  2. 即使當Map完全填充,我們仍然通過Future.get()獲得 的結果。我預計這很便宜,但它很醜。

我想是一個Map,這將阻止得到對於給定的關鍵直到鍵都有一個值來代替cache,但允許其他同時獲得。有這樣的事嗎?還是有人有一個更清潔的替代MapFutures

+2

Store中的關鍵對象的'ConcurrentHashMap'和重點對象本身鎖?如果鍵是內在的(int,'String'等),則將它們包裝起來。 –

+2

這幾乎聽起來像你想要一個番石榴['條紋'](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/util/concurrent/Striped.html ),這種行爲是從對象到鎖的映射。 –

回答

7

爲每個按鍵創建鎖定聽起來很誘人,但它可能不是您想要的,特別是當按鍵數量很大時。

正如您可能需要爲每個鍵創建專用(讀寫)鎖一樣,它會影響您的內存使用情況。而且,如果併發性確實很高,那麼如果內核數量有限,那麼細粒度可能會達到收益遞減點。

在像這樣的情況下,ConcurrentHashMap經常是一個很好的解決方案。它通常提供完整的讀取器併發性(通常讀取器不會阻塞),並且更新可以併發到達期望的併發級別。這給你很好的可伸縮性。上面的代碼可以與ConcurrentHashMap的表達如下所示:

ConcurrentMap<Key,Foo> cache = new ConcurrentHashMap<>(); 
... 
Foo result = cache.get(key); 
if (result == null) { 
    result = createFooExpensively(key); 
    Foo old = cache.putIfAbsent(key, result); 
    if (old != null) { 
    result = old; 
    } 
} 

的簡單使用的ConcurrentHashMap確實有一個缺點,這是多個線程可能會發現該鍵沒有被緩存,並且每一個可以調用createFooExpensively() 。因此,一些線程可能會丟掉工作。爲了避免這種情況,您需要使用「實踐中的Java併發」中提到的備忘錄模式。

不過話又說回來,在谷歌的漂亮的人已經解決了這些問題,爲您在CacheBuilder形式:http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/cache/CacheBuilder.html

LoadingCache<Key,Foo> cache = CacheBuilder.newBuilder(). 
    concurrencyLevel(32). 
    build(new CacheLoader<Key,Foo>() { 
    public Foo load(Key key) { 
     return createFooExpensively(key); 
    } 
    }); 

... 
Foo result = cache.get(key); 
+0

謝謝--JCiP中的「Memoizer」部分幾乎正是這種情況。我應該有RTFB。 :) –

+0

(當我回到可以訪問Guava的代碼中時,我會記得'CacheBuilder'。) –

1

您可以使用funtom-java-utils - PerKeySynchronizedExecutor

它會爲每個按鍵創建一個鎖,但會在它變爲未使用時立即將其清除。

它還將授權調用相同的密鑰之間的內存可見性,並被設計爲非常快速並最大限度地減少不同密鑰之間的調用之間的爭用。

聲明它在你的類:

final PerKeySynchronizedExecutor<KEY_CLASS> executor = new PerKeySynchronizedExecutor<>(); 

使用它:

Foo foo = executor.execute(key,() -> createFooExpensively());