2012-01-19 15 views
5

necesito escribir una aplicación un tanto específica de una memoria caché, que tiene las claves únicas, pero puede contener valores duplicados, por ejemplo:uso de multiproceso de `iteradores ConcurrentHashMap`

"/path/to/one" -> 1 
"/path/to/two" -> 2 
"/path/to/vienas" -> 1 
"/path/to/du" -> 2 

La clase tiene que ofrecer no lee bloqueo/búsquedas clave, pero también tiene mutadores típicos de crear/actualizar/eliminar. Por ejemplo, el valor de la eliminación de 2 debería resultar

"/path/to/one" -> 1 
"/path/to/vienas" -> 1 

Lecturas para este caché serán mayores que las escrituras, con mucho, por lo que el rendimiento de escritura no es un problema - siempre y escrituras concurrentes no se ejecutan en uno encima del otro. Es probable que la cantidad total de entradas sea inferior a 1000, por lo que la iteración ocasional de los valores sigue siendo asequible.

así que escribí algo como esto (pseudo código):

// 
// tl;dr all writes are synchronized on a single lock and each 
// resets the reference to the volatile immutable map after finishing 
// 
class CopyOnWriteCache { 
    private volatile Map<K, V> readOnlyMap = ImmutableMap.of(); 

    private final Object writeLock = new Object(); 

    public void add(CacheEntry entry) { 
     synchronized (writeLock) { 
     readOnlyMap = new ImmutableMap.Builder<K, V>() 
      .addAll(readOnlyMap) 
      .add(entry.key, entry.value) 
      .build(); 
     } 
    } 

    public void remove(CacheEntry entry) { 
     synchronized (writeLock) { 
     Map<K, V> filtered = Maps.filterValues(readOnlyMap, somePredicate(entry)); 
     readOnlyMap = ImmutableMap.copyOf(filtered); 
     } 
    } 

    public void update(CacheEntry entry) { 
     synchronized (writeLock) { 
     Map<K, V> filtered = Maps.filterValues(readOnlyMap, somePredicate(entry)); 
     readOnlyMap = new ImmutableMap.Builder<K, V>() 
      .addAll(filtered) 
      .add(entry.key, entry.value) 
      .build(); 
     } 
    } 

    public SomeValue lookup(K key) { 
     return readOnlyMap.get(key); 
    } 
} 

Después de escribir lo anterior, me di cuenta de que ConcurrentHashMap también ofrece sin bloqueo lee lo que haría que todo mi esfuerzo inútil, pero hay una declaración en su Javadoc, que levanta una ceja:

iterators are designed to be used by only one thread at a time 

Así que si puedo reemplazar el uso de volatile ImmutableMap con final ConcurrentHashMap y eliminar todas las synchronized bloques, es posible que compiten mutators concurrentes se invalidar el uno al otro? Por ejemplo, puedo imaginar cómo dos llamadas concurrentes a remove conducirían a una condición de carrera, invalidando por completo los resultados del primer remove.

La única mejora que puedo ver es que mediante el uso final ConcurrentHashMapy dejando synchronized ya que son por lo menos podría evitar la copia innecesaria de datos.

¿Tiene sentido, o tal vez estoy pasando por alto algo aquí? ¿Alguien puede sugerir otras alternativas para esta solución?

Respuesta

5

Si hizo este reemplazo, aún tendría solo un hilo con un iterador dado a la vez. La advertencia significa que dos subprocesos no deben usar la misma instancia de iterador. No es que dos subprocesos no puedan iterar al mismo tiempo.

El problema que tendría es que, dado que la operación de eliminación no se puede realizar en una sola operación atómica de ConcurrentMap, podría tener un subproceso simultáneo para ver el mapa en un estado intermedio: se ha eliminado un valor pero no el otro.

No estoy seguro de que esto sea más rápido, ya que dice que el rendimiento de escritura no es un problema, pero lo que podría hacer para evitar la copia del mapa en cada escritura, es usar ReadWriteLock que protege un mutable ConcurrentMap. Todas las lecturas seguirán siendo concurrentes, pero una escritura en el mapa evitará que todos los demás subprocesos accedan al mapa. Y no tendrías que crear una copia nueva cada vez que se modifique.

2

Está bien para mutar ConcurrentHashMap a través de múltiples iteradores/múltiples hilos al mismo tiempo. Es solo que no se supone que debes entregar una sola instancia de iterador a múltiples hilos y usarlo al mismo tiempo.

Por lo tanto, si usa ConcurrentHashMap, no necesita dejar synchronized allí. Como señala JB Nizet, la diferencia entre esta y su implementación actual es la visibilidad del estado intermedio.Si no te importa eso, usar ConcurrentHashMap sería mi elección preferida ya que la implementación es más simple y no tendrás que preocuparte por el rendimiento de lectura o escritura.