2013-05-29 12 views
6

Ich habe es mit Code von Drittanbietern zu tun, bei dem teure Objekte erstellt und in einer Map zwischengespeichert werden. Die bestehende Implementierung ist so etwas wiePro-Key-Blockierungskarte in Java

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

Offensichtlich ist dies nicht das beste Design ist, wenn Foos für verschiedene keys können unabhängig erstellt werden.

Mein aktueller Hack ist ein verwenden Map von Futures:

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

Aber das scheint ... ein wenig hacky, aus zwei Gründen:

  1. Die Arbeit an einer beliebigen gepoolten erfolgt Faden. Ich würde mich freuen, die Arbeit auf dem ersten Thread zu haben, der versucht, diesen bestimmten Schlüssel zu bekommen, besonders seit wird es sowieso blockiert werden.
  2. Auch wenn die Map vollständig bestückt ist, gehen wir noch durch Future.get(), um die Ergebnisse zu erhalten. Ich denke, das ist ziemlich billig, aber es ist hässlich.

Was würde Ich mag ist cache mit einem Map zu ersetzen, die für einen bestimmten Schlüssel wird blockiert, bis dieser Schlüssel einen Wert hat, aber zulassen, dass andere mittlerweile bekommt. Gibt es so etwas? Oder hat jemand eine sauberere Alternative zu Map von Futures?

+2

Shop die wichtigsten Objekte in einem 'ConcurrentHashMap' und sperre auf die wichtigsten Objekte selbst? Wenn die Schlüssel intrinsics (int, 'String', usw.) sind, wickle sie um. –

+2

Das klingt fast so, als ob Sie eine Guave ['Striped'] möchten (http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/util/concurrent/Striped.html), die als eine Karte von Objekten zu Sperren fungiert. –

Antwort

7

Das Erstellen einer Sperre pro Taste klingt verlockend, ist jedoch möglicherweise nicht das, was Sie möchten, insbesondere wenn die Anzahl der Tasten groß ist.

Da Sie wahrscheinlich für jeden Schlüssel eine dedizierte (Lese-/Schreib-) Sperre erstellen müssen, hat dies Auswirkungen auf die Speichernutzung. Außerdem kann diese feine Granularität einen Punkt abnehmender Erträge bei einer endlichen Anzahl von Kernen erreichen, wenn die Gleichzeitigkeit wirklich hoch ist.

ConcurrentHashMap ist oft eine gute Lösung in einer Situation wie dieser. Es bietet normalerweise die volle Leser-Nebenläufigkeit (normalerweise blockieren die Leser nicht), und Aktualisierungen können bis zu der Ebene des gewünschten Gleichzeitigkeitsgrades gleichzeitig erfolgen. Dies gibt Ihnen eine ziemlich gute Skalierbarkeit. Der obige Code kann mit ConcurrentHashMap wie folgt ausgedrückt werden:

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

Die einfache Verwendung von ConcurrentHashMap hat einen Nachteil haben, das ist, dass mehrere Threads können feststellen, dass der Schlüssel nicht im Cache gespeichert wird, und jeder kann createFooExpensively aufrufen() . Infolgedessen können einige Threads Arbeit wegwerfen. Um dies zu vermeiden, möchten Sie das Memoizer-Muster verwenden, das in "Java Concurrency in Practice" erwähnt wird.

Aber dann wieder, lösten die netten Leute bei Google bereits diese Probleme für Sie in Form von 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

Danke - die 'Memoizer'-Sektion in JCiP läuft fast genau durch dieses Szenario. Sollte RTFB haben, denke ich. :) –

+0

(Und ich erinnere mich 'CacheBuilder', wenn ich wieder in unserem eigenen Code mit Zugang zu Guava bin.) –

1

Sie funtom-java-utils verwenden können - PerKeySynchronizedExecutor.

Es wird eine Sperre für jeden Schlüssel erstellt, aber es wird sofort für Sie gelöscht, wenn es unbenutzt wird.

Es gewährt auch Speicher Sichtbarkeit zwischen Aufrufen mit dem gleichen Schlüssel, und ist entworfen, um sehr schnell zu sein und den Konflikt zwischen Aufrufen von verschiedenen Schlüsseln zu minimieren.

Deklarieren es in Ihrer Klasse:

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

es verwenden:

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