¿Está configurado el siguiente código para sincronizar correctamente las llamadas synchronizedMap
?
public class MyClass {
private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());
public void doWork(String key) {
List<String> values = null;
while ((values = synchronizedMap.remove(key)) != null) {
//do something with values
}
}
public static void addToMap(String key, String value) {
synchronized (synchronizedMap) {
if (synchronizedMap.containsKey(key)) {
synchronizedMap.get(key).add(value);
}
else {
List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, valuesList);
}
}
}
}
Según tengo entendido, necesito el bloque sincronizado addToMap()
para evitar que otro subproceso llame remove()
o containsKey()
antes de realizar la llamada a, put()
pero no necesito un bloque sincronizado doWork()
porque otro subproceso no puede ingresar al bloque sincronizado addToMap()
antes de las remove()
devoluciones porque creé el mapa originalmente con Collections.synchronizedMap()
. ¿Es eso correcto? ¿Hay una mejor manera de hacer esto?
fuente
Collections.synchronizedMap()
? No entiendo el segundo punto.Si está utilizando JDK 6, es posible que desee consultar ConcurrentHashMap
Tenga en cuenta el método putIfAbsent en esa clase.
fuente
Existe la posibilidad de un error sutil en su código.
[ ACTUALIZACIÓN: Ya que está usando map.remove () esta descripción no es totalmente válida. Me perdí ese hecho la primera vez. :( Gracias al autor de la pregunta por señalar eso. Dejo el resto como está, pero cambié la declaración principal para decir que hay un error potencial .]
En doWork () obtienes el valor de Lista del Mapa de una manera segura para subprocesos. Después, sin embargo, está accediendo a esa lista en un asunto inseguro. Por ejemplo, un hilo puede estar usando la lista en doWork () mientras que otro hilo invoca synchronizedMap.get (key) .add (value) en addToMap () . Esos dos accesos no están sincronizados. La regla general es que las garantías de seguridad para subprocesos de una colección no se extienden a las claves o valores que almacenan.
Puede solucionar este problema insertando una lista sincronizada en el mapa como
List<String> valuesList = new ArrayList<String>(); valuesList.add(value); synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list
Alternativamente, puede sincronizar en el mapa mientras accede a la lista en doWork () :
public void doWork(String key) { List<String> values = null; while ((values = synchronizedMap.remove(key)) != null) { synchronized (synchronizedMap) { //do something with values } } }
La última opción limitará un poco la concurrencia, pero es algo más clara en mi opinión.
Además, una nota rápida sobre ConcurrentHashMap. Esta es una clase realmente útil, pero no siempre es un reemplazo apropiado para HashMaps sincronizados. Citando de sus Javadocs,
En otras palabras, putIfAbsent () es ideal para inserciones atómicas pero no garantiza que otras partes del mapa no cambiarán durante esa llamada; garantiza solo la atomicidad. En su programa de muestra, está confiando en los detalles de sincronización de (un sincronizado) HashMap para otras cosas que no sean put ().
Última cosa. :) Esta gran cita de Java Concurrency in Practice siempre me ayuda a diseñar programas de depuración de múltiples subprocesos.
fuente
Sí, estás sincronizando correctamente. Explicaré esto con más detalle. Debe sincronizar dos o más llamadas a métodos en el objeto synchronizedMap solo en el caso de que tenga que confiar en los resultados de llamadas a métodos anteriores en la siguiente llamada al método en la secuencia de llamadas a métodos en el objeto synchronizedMap. Echemos un vistazo a este código:
synchronized (synchronizedMap) { if (synchronizedMap.containsKey(key)) { synchronizedMap.get(key).add(value); } else { List<String> valuesList = new ArrayList<String>(); valuesList.add(value); synchronizedMap.put(key, valuesList); } }
En este código
y
las llamadas a métodos se basan en el resultado de la anterior
llamada al método.
Si la secuencia de llamadas al método no se sincronizó, el resultado podría ser incorrecto. Por ejemplo,
thread 1
está ejecutando el métodoaddToMap()
ythread 2
está ejecutando el métododoWork()
La secuencia de llamadas al método en elsynchronizedMap
objeto podría ser la siguiente:Thread 1
ha ejecutado el métodoy el resultado es "
true
". Después de que el sistema operativo haya cambiado el control de ejecuciónthread 2
y haya ejecutadoDespués de que el control de ejecución se haya cambiado de nuevo a
thread 1
y se haya ejecutado, por ejemplocreyendo que el
synchronizedMap
objeto contiene elkey
y seNullPointerException
lanzará porquesynchronizedMap.get(key)
regresaránull
. Si la secuencia de llamadas al método en elsynchronizedMap
objeto no depende de los resultados de cada uno, no es necesario sincronizar la secuencia. Por ejemplo, no es necesario sincronizar esta secuencia:aquí
La llamada al método no se basa en los resultados de la
llamada al método (no le importa si algún hilo ha interferido entre las dos llamadas al método y, por ejemplo, ha eliminado el
key1
).fuente
Eso me parece correcto. Si tuviera que cambiar algo, dejaría de usar Collections.synchronizedMap () y sincronizaría todo de la misma manera, solo para que quede más claro.
Además, reemplazaría
if (synchronizedMap.containsKey(key)) { synchronizedMap.get(key).add(value); } else { List<String> valuesList = new ArrayList<String>(); valuesList.add(value); synchronizedMap.put(key, valuesList); }
con
List<String> valuesList = synchronziedMap.get(key); if (valuesList == null) { valuesList = new ArrayList<String>(); synchronziedMap.put(key, valuesList); } valuesList.add(value);
fuente
Collections.synchronizedXXX()
API cuando todavía tenemos que sincronizar en algún objeto (que será solo la colección en la mayoría de los casos) en la lógica de nuestra aplicación diariaLa forma en que ha sincronizado es correcta. Pero hay una trampa
Sin embargo, en el mundo real, generalmente consultaría el mapa antes de ingresar el valor. Por lo tanto, necesitaría realizar dos operaciones y, por lo tanto, se necesita un bloque sincronizado. Entonces, la forma en que lo ha usado es correcta. Sin embargo.
a. Tiene una API 'putIfAbsent' que haría lo mismo pero de una manera más eficiente.
segundo. Es eficiente: dThe CocurrentMap simplemente bloquea las teclas, por lo que no bloquea todo el mundo del mapa. Donde ya ha bloqueado claves y valores.
C. Podría haber pasado la referencia de su objeto de mapa en otro lugar de su base de código donde usted / otro desarrollador en su tean puede terminar usándolo incorrectamente. Es decir, puede que todos agreguen () u obtengan () sin bloquear el objeto del mapa. Por lo tanto, su llamada no se ejecutará de forma mutuamente exclusiva para su bloque de sincronización. Pero el uso de una implementación simultánea le da la tranquilidad de que nunca se puede usar / implementar incorrectamente.
fuente
Echa un vistazo a Google Colecciones '
Multimap
, por ejemplo, la página 28 de esta presentación .Si no puede usar esa biblioteca por alguna razón, considere usar en
ConcurrentHashMap
lugar deSynchronizedHashMap
; tiene unputIfAbsent(K,V)
método ingenioso con el que puede agregar atómicamente la lista de elementos si aún no está allí. Además, considere usarCopyOnWriteArrayList
para los valores del mapa si sus patrones de uso lo justifican.fuente