31

Tengo una aplicación web en la que estoy realizando pruebas de carga/rendimiento, especialmente en una función en la que esperamos que unos pocos cientos de usuarios accedan a la misma página y actualicen todos los elementos 10 segundos en esta página. Un área de mejora que encontramos que podíamos hacer con esta función era almacenar en caché las respuestas del servicio web durante un período de tiempo, ya que los datos no están cambiando.Sincronización en objetos String en Java

Después de implementar este almacenamiento en caché básico, en algunas pruebas adicionales descubrí que no tenía en cuenta cómo los subprocesos simultáneos podían acceder al caché al mismo tiempo. Descubrí que en cuestión de ~ 100 ms, aproximadamente 50 hilos intentaban recuperar el objeto de la memoria caché, encontrando que había expirado, presionando el servicio web para recuperar los datos, y luego colocando el objeto nuevamente en la memoria caché.

El código original parecía algo como esto:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 

    final String key = "Data-" + email; 
    SomeData[] data = (SomeData[]) StaticCache.get(key); 

    if (data == null) { 
     data = service.getSomeDataForEmail(email); 

     StaticCache.set(key, data, CACHE_TIME); 
    } 
    else { 
     logger.debug("getSomeDataForEmail: using cached object"); 
    } 

    return data; 
} 

Por lo tanto, para asegurarse de que sólo un hilo estaba llamando al servicio web cuando el objeto en key expiró, pensé que necesitaba para sincronizar la caché obtener/estableció la operación, y parecía que usar la clave de caché sería un buen candidato para que un objeto se sincronice (de esta forma, las llamadas a este método para el correo electrónico [email protected] no serían bloqueadas por llamadas de método a [email protected])

he actualizado el método para tener este aspecto:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 


    SomeData[] data = null; 
    final String key = "Data-" + email; 

    synchronized(key) {  
    data =(SomeData[]) StaticCache.get(key); 

    if (data == null) { 
     data = service.getSomeDataForEmail(email); 
     StaticCache.set(key, data, CACHE_TIME); 
    } 
    else { 
     logger.debug("getSomeDataForEmail: using cached object"); 
    } 
    } 

    return data; 
} 

También he añadido el registro líneas para cosas como "antes del bloque de sincronización", "dentro de bloque de sincronización", "punto de salir de bloque de sincronización", y " después del bloque de sincronización ", así pude determinar si estaba sincronizando efectivamente la operación get/set.

Sin embargo, no parece que esto haya funcionado. Mis registros de prueba tienen una salida como:

(registro de salida es 'threadName' 'Nombre del registrador' 'mensaje')
http-80-Processor253 jsp.view páginas - getSomeDataForEmail: a punto de entrar en bloque de sincronización
http-80-Processor253-página jsp.view - getSomeDataForEmail: dentro de bloque de sincronización
http-80-Processor253 cache.StaticCache - obtener: objeto en tecla [[email protected]] ha expirado
http-80-Processor253 cache.StaticCache - get: clave [[email protected]] devuelve el valor [nulo]
http-80-Processor263 jsp.view-page - getSomeDataForEmai l: a punto de ingresar al bloque de sincronización
http-80-Processor263 jsp.view-page - getSomeDataForEmail: dentro del bloque de sincronización
http-80-Processor263 cache.StaticCache - get: object at key [[email protected]] ha expirado
http-80-Processor263 cache.StaticCache - get: clave [[email protected]] devuelve el valor [nulo]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: está a punto de ingresar al bloque de sincronización
-página jsp.view http-80-Processor131 - getSomeDataForEmail: dentro de bloque de sincronización
http-80-Processor131 cache.StaticCache - obtener: objeto en tecla [[email protected]] ha expirado
http-80-Processor131 cache.StaticCache - obtener: tecla [[email protected]] valor de volver [nulo]
http-80-Processor104 jsp.view-página - getSomeDataForEmail: dentro de bloque de sincronización
http-80 -Processor104 caché.StaticCache - get: objeto en la clave [[email protected]] ha caducado
http-80-Processor104 cache.StaticCache - get: clave [[email protected]] devuelve el valor [nulo]
http- 80-Processor252 jsp.view-página - getSomeDataForEmail: punto de entrar en bloque de sincronización
http-80-Processor283 jsp.view-página - getSomeDataForEmail: punto de entrar en bloque de sincronización
http-80-Processor2 jsp.view-página - getSomeDataForEmail : punto de entrar en bloque de sincronización
http-80-Processor2-página jsp.view - getSomeDataForEmail: dentro de bloque de sincronización

Quería ver solo un hilo a la vez que ingresaba/salía del bloque de sincronización alrededor de las operaciones get/set.

¿Hay un problema en la sincronización de objetos String? Pensé que la clave de caché sería una buena opción, ya que es única para la operación, y aunque el final String key se haya anunciado en el método, yo estaba pensando que cada hilo estaría consiguiendo una referencia a el mismo objeto y por lo tanto sería sincronización en este único objeto.

¿Qué estoy haciendo mal aquí?

actualización: después de mirar más a fondo en los registros, parece que los métodos con la misma lógica de sincronización, donde la clave es siempre la misma, tales como

final String key = "blah"; 
... 
synchronized(key) { ... 

no presentan el mismo problema de concurrencia - solamente un hilo a la vez está ingresando al bloque.

Actualización 2: ¡Gracias a todos por la ayuda! Me aceptado la primera respuesta sobre intern() ing Cuerdas, que resolvió mi problema inicial - donde varios hilos estaban entrando en bloques sincronizados en el que pensé que no deberían, ya que los key 's tenían el mismo valor.

Como han señalado otros, utilizar intern() para tal propósito y sincronizar en esas cadenas realmente resulta ser una mala idea: al ejecutar pruebas JMeter contra la aplicación web para simular la carga esperada, vi el tamaño de pila utilizado crecen a casi 1GB en poco menos de 20 minutos.

Actualmente estoy usando la solución simple de tan sólo sincronizar el método completo - pero realmente como los ejemplos de código proporcionados por martinprobst y MBCook, pero ya que tengo unos 7 getData() métodos similares de esta clase actualmente (ya que necesita aproximadamente 7 datos diferentes de un servicio web), no quería agregar lógica casi duplicada para obtener y liberar bloqueos para cada método. Pero esta es definitivamente información muy, muy valiosa para uso futuro. Creo que en última instancia, estas son las respuestas correctas sobre la mejor manera de hacer una operación como este thread-safe, ¡y daría más votos a estas respuestas si pudiera!

+0

Usted ya no tendrá que w orry sobre el interno Cadena que cuelga en la memoria: las cadenas aparentemente internados han sido sometidas a GC por bastante tiempo: http://stackoverflow.com/questions/18152560/garbage-collection-on-internd-strings-string- pool-and-perm-space – Volksman

+0

Recomiendo esta respuesta, usando Guava's Striped para evitar el uso excesivo de memoria: https://stackoverflow.com/a/11125602/116810 –

Respuesta

32

Sin poner mi cerebro totalmente en marcha, a partir de un análisis rápido de lo que usted dice parece que lo que necesita pasante() sus cadenas:

final String firstkey = "Data-" + email; 
final String key = firstkey.intern(); 

dos cadenas con el mismo valor son lo contrario no necesariamente el mismo objeto

Tenga en cuenta que esto puede presentar un nuevo punto de contención, ya que en las profundidades de la VM, el interno() puede tener que adquirir un bloqueo.No tengo idea de cómo son las VM modernas en esta área, pero uno espera que estén optimizadas diabólicamente.

Supongo que sabe que StaticCache aún necesita ser seguro para subprocesos. Pero la contención debería ser pequeña comparada con lo que tendrías si estuvieras bloqueando el caché en lugar de solo la clave mientras llamas a getSomeDataForEmail.

Respuesta a la pregunta de actualización:

Creo que se debe a una cadena literal siempre produce el mismo objeto. Dave Costa señala en un comentario que es incluso mejor que eso: un literal siempre cede la representación canónica. Entonces, todos los literales String con el mismo valor en cualquier parte del programa producirían el mismo objeto.

Editar

otros han señalado que de sincronización en las cadenas del interno es en realidad una muy mala idea - en parte debido a la creación de cadenas de internar se le permite hacer que existir en perpetuidad, y en parte porque si hay más de un bit de código en cualquier parte de su programa se sincroniza en cadenas internas, usted tiene dependencias entre esos bits de código, y la prevención de interbloqueos u otros errores puede ser imposible.

Las estrategias para evitar esto almacenando un objeto de bloqueo por cadena de clave se están desarrollando en otras respuestas a medida que escribo.

Aquí hay una alternativa: todavía usa un bloqueo singular, pero sabemos que vamos a necesitar uno de esos para el caché de todos modos, y hablaste de 50 hilos, no 5000, por lo que puede no ser fatal. También estoy asumiendo que el cuello de botella de rendimiento aquí es lento bloqueando I/O en DoSlowThing() que por lo tanto se beneficiará enormemente de no ser serializado. Si ese no es el cuello de botella, entonces:

  • Si la CPU está ocupada, entonces este enfoque puede no ser suficiente y necesita otro enfoque.
  • Si la CPU no está ocupada, y el acceso al servidor no es un cuello de botella, entonces este enfoque es excesivo, y también podría olvidar tanto esto como el bloqueo por clave, poner una gran sincronización (StaticCache) alrededor de toda la operación y hazlo de la manera más fácil.

Obviamente, este enfoque debe ser probado en remojo para la escalabilidad antes de su uso - No garantizo nada.

Este código NO requiere que StaticCache esté sincronizado o de lo contrario sea seguro para subprocesos. Esto debe revisarse si algún otro código (por ejemplo, limpieza programada de datos antiguos) alguna vez toca la caché.

IN_PROGRESS es un valor ficticio, no exactamente limpio, pero el código es simple y ahorra tener dos hashtables. No maneja InterruptedException porque no sé qué quiere hacer su aplicación en ese caso. Además, si DoSlowThing() falla sistemáticamente para una clave dada, este código, tal como está, no es exactamente elegante, ya que cada subproceso volverá a intentarlo. Dado que no sé cuáles son los criterios de falla, y si son susceptibles de ser temporales o permanentes, tampoco manejo esto, solo me aseguro de que los hilos no bloqueen para siempre. En la práctica, es posible que desee colocar un valor de datos en el caché que indique "no disponible", quizás con un motivo, y un tiempo de espera para cuándo volver a intentarlo.se añade

// do not attempt double-check locking here. I mean it. 
synchronized(StaticObject) { 
    data = StaticCache.get(key); 
    while (data == IN_PROGRESS) { 
     // another thread is getting the data 
     StaticObject.wait(); 
     data = StaticCache.get(key); 
    } 
    if (data == null) { 
     // we must get the data 
     StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE); 
    } 
} 
if (data == null) { 
    // we must get the data 
    try { 
     data = server.DoSlowThing(key); 
    } finally { 
     synchronized(StaticObject) { 
      // WARNING: failure here is fatal, and must be allowed to terminate 
      // the app or else waiters will be left forever. Choose a suitable 
      // collection type in which replacing the value for a key is guaranteed. 
      StaticCache.put(key, data, CURRENT_TIME); 
      StaticObject.notifyAll(); 
     } 
    } 
} 

Cada vez que algo en la memoria caché, todas las discusiones se despiertan y comprobar el caché (no importa qué clave que están después), por lo que es posible conseguir un mejor rendimiento con algoritmos menos polémicos. Sin embargo, gran parte de ese trabajo tendrá lugar durante su copioso tiempo de CPU inactivo bloqueando en E/S, por lo que puede no ser un problema.

Este código podría ser común para su uso con cachés múltiples, si define abstracciones adecuadas para la memoria caché y su bloqueo asociado, los datos que devuelve, la variable ficticia IN_PROGRESS y la operación lenta para realizar. Convertir todo en un método en la memoria caché puede no ser una mala idea.

+0

bastante bien, y la razón por la que funciona cuando tienes una constante La clave es que los literales de cadena son automáticamente internados. –

+0

Ah, eso responde a mi impreciso paseo "No puedo recordar". Gracias. –

+0

Llamar a String.intern() en un servidor web donde el caché de cadenas podría vivir indefinidamente no es una buena idea. No dice que esta publicación es incorrecta, pero no es una buena solución para la pregunta original. – McDowell

2

La llamada:

final String key = "Data-" + email; 

crea un nuevo objeto cada vez que se llama al método. Debido a que ese objeto es lo que usa para bloquear, y cada llamada a este método crea un nuevo objeto, entonces no está realmente sincronizando el acceso al mapa en función de la clave.

Esto explica mejor su edición. Cuando tienes una cadena estática, entonces funcionará.

El uso de intern() resuelve el problema, porque devuelve la cadena de un grupo interno mantenido por la clase String, que garantiza que si dos cadenas son iguales, se usará la del grupo. Ver

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()

2

Su principal problema no es sólo que puede haber varias copias de la secuencia con el mismo valor. El principal problema es que necesita tener solo un monitor en el que sincronizar para acceder al objeto StaticCache. De lo contrario, varios subprocesos podrían terminar modificando StaticCache simultáneamente (aunque con diferentes claves), lo que probablemente no sea compatible con la modificación simultánea.

+0

Esto se resuelve haciéndolo usar, por ejemplo, ConcurrentHashmap. –

+0

¡Tienes razón, me olvidé de ConcurrentHashmap! Sin embargo, a juzgar por la actualización de la pregunta, StaticCache realmente no admite modificaciones concurrentes. – Alexander

21

Sincronizar en una cadena interna puede que no sea una buena idea: al internarla, la cadena se convierte en un objeto global, y si se sincroniza en las mismas cadenas internas en diferentes partes de la aplicación, es posible que obtienes problemas de sincronización realmente poco comunes y básicamente indebuggables, como interbloqueos. Puede parecer poco probable, pero cuando sucede estás realmente jodido. Como regla general, solo sincronice en un objeto local donde esté absolutamente seguro de que ningún código fuera de su módulo podría bloquearlo.

En su caso, puede usar una tabla hash sincronizada para almacenar objetos de bloqueo para sus llaves.

ej .:

Object data = StaticCache.get(key, ...); 
if (data == null) { 
    Object lock = lockTable.get(key); 
    if (lock == null) { 
    // we're the only one looking for this 
    lock = new Object(); 
    synchronized(lock) { 
     lockTable.put(key, lock); 
     // get stuff 
     lockTable.remove(key); 
    } 
    } else { 
    synchronized(lock) { 
     // just to wait for the updater 
    } 
    data = StaticCache.get(key); 
    } 
} else { 
    // use from cache 
} 

Este código tiene una condición de carrera, en la que dos hilos podrían poner un objeto en la tabla de bloqueos después de la otra. Sin embargo, esto no debería ser un problema, ya que solo tiene un hilo más que llama al servicio web y actualiza el caché, lo que no debería ser un problema.

Si está invalidando la memoria caché después de un tiempo, debe comprobar si los datos son nulos nuevamente después de recuperarlos de la memoria caché, en el caso! = Null.

Alternativamente, y mucho más fácil, puede sincronizar todo el método de búsqueda de caché ("getSomeDataByEmail"). Esto significa que todos los hilos tienen que sincronizarse cuando acceden a la memoria caché, lo que podría ser un problema de rendimiento. Pero, como siempre, prueba esta solución simple primero y mira si realmente es un problema. En muchos casos, no debería ser así, ya que probablemente dedique mucho más tiempo procesando el resultado que sincronizando.

+0

Pregunta: ¿quiere decir si sincronizo en la MISMA cadena interpuesta en otro lugar de la aplicación? –

+1

+1 para un mapa de cadenas para bloquear objetos – Matt

+0

De acuerdo. Apoyos para señalar el problema y proporcionar una solución bastante buena. Tal vez editar esta respuesta para incluir el código real para sellar el trato? –

0

¿Por qué no simplemente renderizar una página html estática que se sirve al usuario y se regenera cada x minutos?

4

Otros han sugerido el interning the strings, y eso funcionará.

El problema es que Java tiene que mantener cadenas internas. Me dijeron que hace esto incluso si no tienes una referencia porque el valor debe ser el mismo la próxima vez que alguien use esa cadena. Esto significa que todas las cadenas pueden comenzar a consumir memoria, lo que con la carga que describes podría ser un gran problema.

he visto dos soluciones para esto:

Se puede sincronizar en otro objeto

En lugar del correo electrónico, hacer que un objeto que contiene el correo electrónico (decir el objeto de usuario) que mantiene el valor de correo electrónico como una variable. Si ya tiene otro objeto que represente a la persona (supongamos que ya sacó algo del DB en función de su correo electrónico), podría usarlo. Al implementar el método equals y el método hashcode, puedes asegurarte de que Java considera los objetos iguales cuando haces un cache.contains estático() para averiguar si los datos ya están en el caché (deberás sincronizarlos en el caché))

En realidad, puede guardar un segundo Mapa para que los objetos se bloqueen. Algo como esto:

Map<String, Object> emailLocks = new HashMap<String, Object>(); 

Object lock = null; 

synchronized (emailLocks) { 
    lock = emailLocks.get(emailAddress); 

    if (lock == null) { 
     lock = new Object(); 
     emailLocks.put(emailAddress, lock); 
    } 
} 

synchronized (lock) { 
    // See if this email is in the cache 
    // If so, serve that 
    // If not, generate the data 

    // Since each of this person's threads synchronizes on this, they won't run 
    // over eachother. Since this lock is only for this person, it won't effect 
    // other people. The other synchronized block (on emailLocks) is small enough 
    // it shouldn't cause a performance problem. 
} 

Esto evitará 15 recuperaciones en la misma dirección de correo electrónico a la vez. Necesitará algo para evitar que demasiadas entradas terminen en el mapa de emailLocks. Usar LRUMap s de Apache Commons lo haría.

Esto necesitará algunos ajustes, pero puede resolver su problema.

Uso claves diferentes

Si usted está dispuesto a soportar posibles errores (no sé lo importante que es) se puede utilizar el código hash de la cadena como la clave. los ints no necesitan ser internados.

Resumen

espero que esto ayude. Enhebrar es divertido, ¿verdad? También podría usar la sesión para establecer un valor que significa "Ya estoy trabajando para encontrar esto" y verifique si el segundo (tercero, enésimo) subproceso necesita intentar crearlo o simplemente espere a que aparezca el resultado. en el caché Creo que tuve tres sugerencias.

+0

Su correo electrónico Bloquea fugas: este enfoque necesita algún tipo de recuento de referencias y mecanismo de liberación. – McDowell

+1

"las entradas no necesitan ser internados", pero no tienen monitores. Su resumen insinúa el emocionante mundo de wait/notifyAll, que siempre es una risa :-) –

+0

@McDowell: Creo que un WeakHashMap podría ser apropiado aquí. Sin embargo, es una ocurrencia tan rara que no me atrevo a decir nada más ... –

8

Las cadenas son no buenos candidatos para la sincronización. Si debe sincronizar en un String ID, puede hacerlo utilizando la cadena para crear un mutex (consulte "synchronizing on an ID"). Si el costo de ese algoritmo vale la pena depende de si la invocación de su servicio implica alguna E/S significativa.

también:

  • espero que el StaticCache.get() y set() métodos son multi-hilo.
  • String.intern() tiene un costo (que varía según las implementaciones de VM) y debe utilizarse con cuidado.
0

También le sugiero deshacerse de la concatenación de cadenas por completo si no lo necesita.

final String key = "Data-" + email; 

¿Hay otras cosas/tipos de objetos en la memoria caché que utiliza la dirección de correo electrónico que necesita ese extra "Data-" al comienzo de la llave?

si no, yo sólo haría que

final String key = email; 

y evitar todos los que la creación de la cadena extra también.

5

Puede utilizar los 1,5 utilidades de concurrencia para proporcionar una caché diseñado para permitir el acceso simultáneo múltiple, y un único punto de adición (es decir, sólo un hilo cada vez de realizar el objeto caro "creación"):

private ConcurrentMap<String, Future<SomeData[]> cache; 
private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception { 

    final String key = "Data-" + email; 
    Callable<SomeData[]> call = new Callable<SomeData[]>() { 
     public SomeData[] call() { 
      return service.getSomeDataForEmail(email); 
     } 
    } 
    FutureTask<SomeData[]> ft; ; 
    Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic 
    if (f == null) { //this means that the cache had no mapping for the key 
     f = ft; 
     ft.run(); 
    } 
    return f.get(); //wait on the result being available if it is being calculated in another thread 
} 

Obviamente, esto no maneja las excepciones como te gustaría, y el caché no tiene desalojo integrado. Sin embargo, tal vez podrías usarlo como base para cambiar tu clase de StaticCache.

+0

Como le gusta mucho esta solución, no es necesario guardar un mapa de claves separado o en progreso separado para las cerraduras, contiene el 'Futuro' (hay otro hilo trabajando en ello, simplemente llame a 'get') o no lo necesita 't (no hay otros hilos trabajando en él, haga el cálculo caro) – Krease

2

Utilice un marco de caché decente como ehcache.

Implementar un buen caché no es tan fácil como algunas personas creen.

Con respecto al comentario de que String.intern() es una fuente de pérdidas de memoria, eso no es cierto en realidad. Strings Interno son basura recolectada, simplemente podría llevar más tiempo porque en ciertas JVM'S (SUN) se almacenan en el espacio Perm que solo se tocan con los GC completos.

1

Esto es bastante tarde, pero hay un montón de código incorrecto presentado aquí.

En este ejemplo:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 


    SomeData[] data = null; 
    final String key = "Data-" + email; 

    synchronized(key) {  
    data =(SomeData[]) StaticCache.get(key); 

    if (data == null) { 
     data = service.getSomeDataForEmail(email); 
     StaticCache.set(key, data, CACHE_TIME); 
    } 
    else { 
     logger.debug("getSomeDataForEmail: using cached object"); 
    } 
    } 

    return data; 
} 

La sincronización está en el ámbito incorrectamente. Para una memoria caché estática que admita una API get/put, debe haber al menos sincronización en las operaciones de tipo get y getIfAbsentPut, para un acceso seguro a la memoria caché. El alcance de la sincronización será la memoria caché en sí.

Si las actualizaciones se deben realizar en los propios elementos de datos, eso agrega una capa adicional de sincronización, que debe estar en los elementos de datos individuales.

SynchronizedMap se puede utilizar en lugar de la sincronización explícita, pero se debe tener cuidado. Si se utilizan las API incorrectas (get y put en lugar de putIfAbsent), las operaciones no tendrán la sincronización necesaria, a pesar del uso del mapa sincronizado. Observe las complicaciones introducidas por el uso de putIfAbsent: O bien, el valor put debe calcularse incluso en los casos en que no es necesario (porque el put no puede saber si el valor put es necesario hasta que se examinan los contenidos del caché), o requiere un cuidadoso uso de la delegación (digamos, usando Future, que funciona, pero es algo así como un desajuste, ver más abajo), donde el valor de venta se obtiene a pedido, si es necesario.

El uso de Futures es posible, pero parece bastante incómodo, y quizás un poco excesivo de ingeniería.La Future API está en su núcleo para operaciones asincrónicas, en particular, para operaciones que pueden no completarse inmediatamente. Involving Future muy probablemente agrega una capa de creación de subprocesos, probablemente complicaciones adicionales innecesarias.

El principal problema del uso del futuro para este tipo de operación es que el futuro inherentemente se relaciona con el multihilo. El uso de Future cuando un nuevo hilo no es necesario significa ignorar una gran parte de la maquinaria de Future, por lo que es una API demasiado pesada para este uso. otra

0

sincronización manera en objeto de cadena:

String cacheKey = ...; 

    Object obj = cache.get(cacheKey) 

    if(obj==null){ 
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){ 
      obj = cache.get(cacheKey) 
     if(obj==null){ 
      //some cal obtain obj value,and put into cache 
     } 
    } 
} 
+0

Esto tiene los mismos inconvenientes que 'String.intern' con mayor probabilidad de tener problemas. – Vadzim

1

Aquí es una solución segura a corto Java 8 que utiliza un mapa de objetos de bloqueo dedicados para la sincronización:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>(); 

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 
    final String key = "Data-" + email; 
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) { 
     SomeData[] data = StaticCache.get(key); 
     if (data == null) { 
      data = service.getSomeDataForEmail(email); 
      StaticCache.set(key, data); 
     } 
    } 
    return data; 
} 

Tiene el inconveniente de que las llaves y los objetos de bloqueo se mantendrían en el mapa para siempre.

Esto se puede evitar de esta manera:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 
    final String key = "Data-" + email; 
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) { 
     try { 
      SomeData[] data = StaticCache.get(key); 
      if (data == null) { 
       data = service.getSomeDataForEmail(email); 
       StaticCache.set(key, data); 
      } 
     } finally { 
      keyLocks.remove(key); 
     } 
    } 
    return data; 
} 

Pero entonces teclas populares estarían constantemente se vuelven a insertar en el mapa con objetos de bloqueo están reasignando.

Actualización: Y esto deja la posibilidad de condición de carrera cuando dos hilos entrarían simultáneamente en la sección sincronizada para la misma llave pero con diferentes bloqueos.

por lo que puede ser más seguro y eficiente de utilizar expiring Guava Cache:

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder() 
     .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected 
     .build(CacheLoader.from(Object::new)); 

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 
    final String key = "Data-" + email; 
    synchronized (keyLocks.getUnchecked(key)) { 
     SomeData[] data = StaticCache.get(key); 
     if (data == null) { 
      data = service.getSomeDataForEmail(email); 
      StaticCache.set(key, data); 
     } 
    } 
    return data; 
} 

Tenga en cuenta que se asume aquí que StaticCache es seguro para subprocesos y no sufrir de concurrentes lee y escribe para diferentes claves.

0

En caso de que otros tienen un problema similar, el siguiente código funciona, por lo que yo puedo decir:

import java.util.Map; 
import java.util.concurrent.ConcurrentHashMap; 
import java.util.concurrent.atomic.AtomicInteger; 
import java.util.function.Supplier; 

public class KeySynchronizer<T> { 

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>(); 

    public <U> U synchronize(T key, Supplier<U> supplier) { 
     CounterLock lock = locks.compute(key, (k, v) -> 
       v == null ? new CounterLock() : v.increment()); 
     synchronized (lock) { 
      try { 
       return supplier.get(); 
      } finally { 
       if (lock.decrement() == 0) { 
        // Only removes if key still points to the same value, 
        // to avoid issue described below. 
        locks.remove(key, lock); 
       } 
      } 
     } 
    } 

    private static final class CounterLock { 

     private AtomicInteger remaining = new AtomicInteger(1); 

     private CounterLock increment() { 
      // Returning a new CounterLock object if remaining = 0 to ensure that 
      // the lock is not removed in step 5 of the following execution sequence: 
      // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true) 
      // 2) Thread 2 evaluates "v == null" to false in locks.compute 
      // 3) Thread 1 calls lock.decrement() which sets remaining = 0 
      // 4) Thread 2 calls v.increment() in locks.compute 
      // 5) Thread 1 calls locks.remove(key, lock) 
      return remaining.getAndIncrement() == 0 ? new CounterLock() : this; 
     } 

     private int decrement() { 
      return remaining.decrementAndGet(); 
     } 
    } 
} 

En el caso de la OP, que sería utilizado como esto:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>(); 

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { 
    String key = "Data-" + email; 
    return keySynchronizer.synchronize(key,() -> { 
     SomeData[] existing = (SomeData[]) StaticCache.get(key); 
     if (existing == null) { 
      SomeData[] data = service.getSomeDataForEmail(email); 
      StaticCache.set(key, data, CACHE_TIME); 
      return data; 
     } 
     logger.debug("getSomeDataForEmail: using cached object"); 
     return existing; 
    }); 
} 

Si nada debe ser devuelto por el código sincronizado, el método de sincronización se puede escribir así:

public void synchronize(T key, Runnable runnable) { 
    CounterLock lock = locks.compute(key, (k, v) -> 
      v == null ? new CounterLock() : v.increment()); 
    synchronized (lock) { 
     try { 
      runnable.run(); 
     } finally { 
      if (lock.decrement() == 0) { 
       // Only removes if key still points to the same value, 
       // to avoid issue described below. 
       locks.remove(key, lock); 
      } 
     } 
    } 
}