2009-03-18 20 views
25

Duplicar posibles:
What is the best way to increase number of locks in javaSincronización en un valor entero

Supongamos que desea bloquear basa en un valor entero de identificación. En este caso, hay una función que extrae un valor de un caché y realiza una recuperación/almacenamiento bastante caro en el caché si el valor no está allí.

múltiples operaciones El código existente no está sincronizado y potencialmente podría desencadenar recuperar/Tienda:

//psuedocode 
public Page getPage (Integer id){ 
    Page p = cache.get(id); 
    if (p==null) 
    { 
     p=getFromDataBase(id); 
     cache.store(p); 
    } 
} 

Lo que me gustaría hacer es sincronizar el recuperar en la identificación, por ejemplo,

if (p==null) 
    { 
     synchronized (id) 
     { 
     ..retrieve, store 
     } 
    } 

Por desgracia, esto no funcionará porque 2 llamadas separadas pueden tener el mismo valor entero Identificación sino un objeto Integer diferente, por lo que no compartirán la cerradura y ninguna sincronización ocurrirá.

¿Existe una manera simple de asegurar que tiene la misma instancia de Integer? Por ejemplo, es que esto funciona:

syncrhonized (Integer.valueOf(id.intValue())){ 

el Javadoc para Integer.valueOf() parece dar a entender que es muy probable que obtener el mismo ejemplo, pero que no se ve como una garantía:

Devuelve una instancia de Entero que representa el valor int especificado. Si no se requiere una nueva instancia Integer, este método debe generalmente ser utilizado con preferencia a la constructor entero (int), ya que este método es probable que el rendimiento significativamente mejor espacio y el tiempo rendimiento mediante el almacenamiento en caché frecuencia solicitados valores.

¿Alguna sugerencia sobre cómo obtener una instancia de Integer que garantice que es la misma, aparte de las soluciones más elaboradas, como mantener un WeakHashMap de objetos de bloqueo en clave de int? (No hay nada de malo en eso, parece que debe haber un obvio de una línea que me falta).

+2

¡Esta pregunta "duplicada" y la respuesta de Eddie a continuación son mejores que la pregunta "original"! – arun

+0

Vea también http://stackoverflow.com/questions/6616141/java-threads-locking-on-a-specific-object específicamente http://stackoverflow.com/a/27806218/32453 – rogerdpack

+0

¿Qué pasa con el uso de [AtomicInteger] (https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicInteger.html)? – GregT

Respuesta

43

Realmente no desea sincronizar en un Integer, ya que no tiene control sobre qué instancias son iguales y qué instancias son diferentes. Java simplemente no proporciona esa facilidad (a menos que use enteros en un rango pequeño) que es confiable en diferentes JVM. Si realmente debe sincronizar en un Entero, entonces necesita mantener un Mapa o Conjunto de Entero para que pueda garantizar que está obteniendo la instancia exacta que desea.

Mejor sería crear un objeto nuevo, tal vez almacenado en un HashMap que está codificado por Integer, para sincronizar. Algo como esto:

public Page getPage(Integer id) { 
    Page p = cache.get(id); 
    if (p == null) { 
    synchronized (getCacheSyncObject(id)) { 
     p = getFromDataBase(id); 
     cache.store(p); 
    } 
    } 
} 

private ConcurrentMap<Integer, Integer> locks = new ConcurrentHashMap<Integer, Integer>(); 

private Object getCacheSyncObject(final Integer id) { 
    locks.putIfAbsent(id, id); 
    return locks.get(id); 
} 

Para explicar este código, se utiliza ConcurrentMap, que permite el uso de putIfAbsent. Usted puede hacer esto:

locks.putIfAbsent(id, new Object()); 

pero luego se incurrirá en la (pequeña) el costo de la creación de un objeto para cada acceso. Para evitar eso, simplemente guardo el entero en el Map. ¿Qué logra esto? ¿Por qué es esto diferente de solo usar el Integer en sí?

Cuando haces un get() de un Map, las claves se comparan con equals() (o, al menos, el método utilizado es el equivalente de usar equals()). Dos instancias de enteros diferentes del mismo valor serán iguales entre sí. Por lo tanto, puede pasar cualquier cantidad de instancias de Entero diferentes de "new Integer(5)" como el parámetro a getCacheSyncObject y siempre obtendrá solo la primera instancia que se pasó en ese valor.

Hay razones por las que puede no desea sincronizar en Integer ... se puede conseguir en los puntos muertos si varios subprocesos se sincronizan en Integer objetos y por lo tanto, sin saberlo, utilizando las mismas cerraduras cuando quieren utilizar las cerraduras diferentes. Puede solucionar este riesgo mediante el uso de la versión

locks.putIfAbsent(id, new Object()); 

y por lo tanto incurrir en una (muy) pequeño costo para cada acceso a la caché. Al hacer esto, usted garantiza que esta clase hará su sincronización en un objeto que no se sincronizará en ninguna otra clase. Siempre es una buena cosa.

+0

Un implementador tendría que expandir esta solución para sincronizar el mapa de bloqueos y agregar algún tipo de mecanismo de recuento o liberación de referencia. – McDowell

+0

Sí, si un elemento se libera de la memoria caché de página, entonces desea liberar el bloqueo que se utilizó para ese elemento de memoria caché de página. – Eddie

+0

¿Hay una carrera dentro de getCacheSyncObject? ¿La sincronización tiene efecto en el resultado de la llamada u otro objeto para calcular el valor de la expresión? Hay una apertura donde podría terminar sincronizándose en 2 objetos diferentes. El putIfAbsent de otra solución probablemente funcione. – johnny

3

Integer.valueOf() solo devuelve instancias en caché para un rango limitado. No ha especificado su rango, pero en general, esto no funcionará.

Sin embargo, le recomiendo que no tome este enfoque, incluso si sus valores están en el rango correcto. Como estas instancias de Integer almacenadas en caché están disponibles para cualquier código, no puede controlar completamente la sincronización, lo que podría provocar un interbloqueo. Este es el mismo problema que las personas intentan bloquear con el resultado String.intern().

El mejor bloqueo es una variable privada. Como solo su código puede hacer referencia a él, puede garantizar que no se produzcan bloqueos. Por supuesto, usar WeakHashMap tampoco funcionará. Si la instancia que sirve como clave no tiene referencia, será basura recolectada. Y si está fuertemente referenciado, puede usarlo directamente.

1

¿Qué tal un ConcurrentHashMap con los objetos enteros como claves?

+0

Sí, como mencioné, podría usar algún tipo de mapa donde los objetos serían bloqueos privados. Estaba más interesado en saber si había alguna forma en Java de garantizar una instancia de enteros específica. –

1

Puede echar un vistazo a this code para crear un mutex a partir de una ID. El código se escribió para las ID de cadena, pero podría editarse fácilmente para objetos enteros.

3

El uso de sincronización en un entero suena realmente mal por diseño.

Si necesita sincronizar cada elemento individualmente solo durante la recuperación/almacenamiento, puede crear un conjunto y almacenar allí los elementos actualmente bloqueados. En otras palabras,

// this contains only those IDs that are currently locked, that is, this 
// will contain only very few IDs most of the time 
Set<Integer> activeIds = ... 

Object retrieve(Integer id) { 
    // acquire "lock" on item #id 
    synchronized(activeIds) { 
     while(activeIds.contains(id)) { 
      try { 
       activeIds.wait(); 
      } catch(InterruptedExcption e){...} 
     } 
     activeIds.add(id); 
    } 
    try { 

     // do the retrieve here... 

     return value; 

    } finally { 
     // release lock on item #id 
     synchronized(activeIds) { 
      activeIds.remove(id); 
      activeIds.notifyAll(); 
     } 
    } 
} 

Lo mismo va a la tienda.

La conclusión es: no hay una sola línea de código que resuelva este problema exactamente de la manera que necesita.

+0

+1 Me gusta esencialmente este enfoque. Solo existe el problema potencial que menciono en mi publicación a continuación, que bajo gran controversia, los hilos de espera para TODOS los identificadores se despiertan cuando cualquier ID está disponible. –

+0

Esto puede no ser un problema real si no tiene demasiados hilos. Pero si ese es el caso, puede usar Object.notify() y usar Object.wait (maxTime) solo para asegurarse de que ninguna persona que llama se quede atrapada. – Antonio

+0

De acuerdo probablemente no es un gran problema bajo una baja contención. Lamentablemente, creo que la solución que mencionas podría ser peor ... –

4

Utilice un mapa seguro para subprocesos, como ConcurrentHashMap. Esto le permitirá manipular un mapa de forma segura, pero use un bloqueo diferente para realizar el cálculo real.De esta forma, puede tener múltiples cálculos funcionando simultáneamente con un solo mapa.

Use ConcurrentMap.putIfAbsent, pero en lugar de colocar el valor real, utilice un Future con una construcción computacionalmente liviana. Posiblemente la implementación FutureTask. Ejecute el cálculo y luego get el resultado, que se bloqueará de manera segura hasta que esté listo.

1

Como se puede ver en la variedad de respuestas, hay varias maneras de pelar este gato:

  • Goetz et de al enfoque de mantener una caché de FutureTasks funciona bastante bien en situaciones como esto donde "guardas algo de todos modos" así que no te molesta construir un mapa de objetos FutureTask (y si te importa crecer el mapa, al menos es fácil hacer una poda concurrente)
  • Como respuesta general a "cómo bloquear la identificación", el enfoque descrito por Antonio tiene la ventaja de es obvio cuando se agrega/elimina el mapa de bloqueos.

Es posible que tenga que estar atento a un posible problema con aplicación de Antonio, a saber, que la notifyAll() se despertará hilos esperando en todos identificadores cuando uno de ellos esté disponible, que no podrá escala muy bien bajo alta contención. En principio, creo que puedes solucionarlo teniendo un objeto Condition para cada ID actualmente bloqueada, que es lo que estás esperando/señal. Por supuesto, si en la práctica rara vez se espera más de una identificación en un momento dado, entonces esto no es un problema.

1

Steve,

su código propuesto tiene un montón de problemas con la sincronización. (Antonio también lo hace).

En resumen:

  1. Usted necesita para almacenar en caché un costoso objeto.
  2. Debe asegurarse de que mientras un hilo está realizando la recuperación, otro hilo tampoco intenta recuperar el mismo objeto.
  3. Que para n-hilos todos los intentos de obtener el objeto solo 1 objeto alguna vez se recupera y se devuelve.
  4. Eso para los hilos que solicitan objetos diferentes que no compiten entre sí.

pseudo código para que esto suceda (utilizando un ConcurrentHashMap como la memoria caché):

ConcurrentMap<Integer, java.util.concurrent.Future<Page>> cache = new ConcurrentHashMap<Integer, java.util.concurrent.Future<Page>>; 

public Page getPage(Integer id) { 
    Future<Page> myFuture = new Future<Page>(); 
    cache.putIfAbsent(id, myFuture); 
    Future<Page> actualFuture = cache.get(id); 
    if (actualFuture == myFuture) { 
     // I am the first w00t! 
     Page page = getFromDataBase(id); 
     myFuture.set(page); 
    } 
    return actualFuture.get(); 
} 

Nota:

  1. java.util.concurrent.Future es una interfaz
  2. java .util.concurrent.Futuro en realidad no tiene un conjunto() pero mira las clases existentes que implementan Future para entender cómo implementar tu propio futuro (O usa FutureTask)
  3. Es casi seguro que sería una buena idea empujar la recuperación real a un hilo de trabajo.
Cuestiones relacionadas