Introducción a la creación de bandas de bloqueo

1. Introducción

En este tutorial, aprenderemos cómo lograr una sincronización detallada, también conocida como Lock Striping, un patrón para manejar el acceso concurrente a las estructuras de datos mientras se mantiene un buen rendimiento.

2. El problema

HashMap no es una estructura de datos segura para subprocesos debido a su naturaleza no sincronizada. Eso significa que los comandos de un entorno de subprocesos múltiples pueden resultar en inconsistencia de datos.

Para superar ese problema, podemos convertir el mapa original con el método Collections # synchronizedMap o usar la estructura de datos HashTable . Ambos devolverán una implementación segura para subprocesos de la interfaz del mapa , pero tienen un costo de rendimiento.

El enfoque de definir el acceso exclusivo sobre estructuras de datos con un único objeto de bloqueo se denomina sincronización de grano grueso .

En una implementación de sincronización de grano grueso, cada acceso al objeto debe realizarse a la vez por un hilo. Terminamos teniendo accesos secuenciales.

Nuestro objetivo es permitir que los subprocesos simultáneos funcionen en la estructura de datos al tiempo que garantizamos la seguridad de los subprocesos.

3. Bloqueo de bandas

Para alcanzar nuestro objetivo, usaremos el patrón Lock Striping. La creación de bandas de bloqueo es una técnica en la que el bloqueo se produce en varios depósitos o franjas, lo que significa que acceder a un depósito solo bloquea ese depósito y no toda la estructura de datos.

Hay dos formas de hacer esto:

  • Primero, podríamos usar un bloqueo por tarea, maximizando así la concurrencia entre tareas, aunque esto tiene una mayor huella de memoria.
  • O bien, podríamos usar un solo bloqueo para cada tarea, lo que hace uso de menos memoria pero también compromete el rendimiento en concurrencia

Para ayudarnos a administrar esta compensación rendimiento-memoria, Guava se envía con una clase llamada Striped. Es similar a la lógica que se encuentra en ConcurrentHashMap , pero la clase Striped va aún más lejos al reducir la sincronización de distintas tareas mediante semáforos o bloqueos reentrantes.

4. Un ejemplo rápido

Hagamos un ejemplo rápido para ayudarnos a comprender los beneficios de este patrón.

Compararemos HashMap frente a ConcurrentHashMap y un solo bloqueo frente a un bloqueo rayado, lo que da como resultado cuatro experimentos.

Para cada experimento, realizaremos lecturas y escrituras simultáneas en el mapa subyacente . Lo que variará es cómo accedemos a cada depósito.

Y para eso, crearemos dos clases: SingleLock y StripedLock. Estas son implementaciones concretas de una clase abstracta ConcurrentAccessExperiment que hace el trabajo.

4.1. Dependencias

Como vamos a usar la clase Striped de Guava , agregaremos la dependencia de guava :

 com.google.guava guava 28.2-jre 

4.2. Proceso principal

Nuestra clase ConcurrentAccessExperiment implementa el comportamiento descrito anteriormente:

public abstract class ConcurrentAccessExperiment { public final Map doWork(Map map, int threads, int slots) { CompletableFuture[] requests = new CompletableFuture[threads * slots]; for (int i = 0; i < threads; i++) { requests[slots * i + 0] = CompletableFuture.supplyAsync(putSupplier(map, i)); requests[slots * i + 1] = CompletableFuture.supplyAsync(getSupplier(map, i)); requests[slots * i + 2] = CompletableFuture.supplyAsync(getSupplier(map, i)); requests[slots * i + 3] = CompletableFuture.supplyAsync(getSupplier(map, i)); } CompletableFuture.allOf(requests).join(); return map; } protected abstract Supplier putSupplier(Map map, int key); protected abstract Supplier getSupplier(Map map, int key); }

Es importante tener en cuenta que, como nuestra prueba está vinculada a la CPU, hemos limitado la cantidad de cubos a varios de los procesadores disponibles.

4.3. Acceso concurrente con ReentrantLock

Ahora implementaremos los métodos para nuestras tareas asincrónicas.

Nuestra clase SingleLock define un solo bloqueo para toda la estructura de datos utilizando ReentrantLock :

public class SingleLock extends ConcurrentAccessExperiment { ReentrantLock lock; public SingleLock() { lock = new ReentrantLock(); } protected Supplier putSupplier(Map map, int key) { return (()-> { lock.lock(); try { return map.put("key" + key, "value" + key); } finally { lock.unlock(); } }); } protected Supplier getSupplier(Map map, int key) { return (()-> { lock.lock(); try { return map.get("key" + key); } finally { lock.unlock(); } }); } }

4.4. Acceso concurrente con rayas

Luego, la clase StripedLock define un candado rayado para cada depósito:

public class StripedLock extends ConcurrentAccessExperiment { Striped lock; public StripedLock(int buckets) { lock = Striped.lock(buckets); } protected Supplier putSupplier(Map map, int key) { return (()-> { int bucket = key % stripedLock.size(); Lock lock = stripedLock.get(bucket); lock.lock(); try { return map.put("key" + key, "value" + key); } finally { lock.unlock(); } }); } protected Supplier getSupplier(Map map, int key) { return (()-> { int bucket = key % stripedLock.size(); Lock lock = stripedLock.get(bucket); lock.lock(); try { return map.get("key" + key); } finally { lock.unlock(); } }); } }

Entonces, ¿qué estrategia funciona mejor?

5. Resultados

Usemos JMH (Java Microbenchmark Harness) para averiguarlo. Los puntos de referencia se pueden encontrar a través del enlace del código fuente al final del tutorial.

Al ejecutar nuestro punto de referencia, podemos ver algo similar a lo siguiente (tenga en cuenta que un mayor rendimiento es mejor):

Benchmark Mode Cnt Score Error Units ConcurrentAccessBenchmark.singleLockConcurrentHashMap thrpt 10 0,059 ± 0,006 ops/ms ConcurrentAccessBenchmark.singleLockHashMap thrpt 10 0,061 ± 0,005 ops/ms ConcurrentAccessBenchmark.stripedLockConcurrentHashMap thrpt 10 0,065 ± 0,009 ops/ms ConcurrentAccessBenchmark.stripedLockHashMap thrpt 10 0,068 ± 0,008 ops/ms 

6. Conclusiones

En este tutorial, exploramos diferentes formas de cómo podemos lograr un mejor rendimiento utilizando Lock Striping en estructuras similares a mapas . Creamos un punto de referencia para comparar los resultados con varias implementaciones.

A partir de nuestros resultados de referencia, podemos comprender cómo diferentes estrategias concurrentes podrían afectar significativamente el proceso general. El patrón Striped Lock otorga una gran mejora, ya que obtiene una puntuación de ~ 10% adicional con HashMap y ConcurrentHashMap .

Como de costumbre, el código fuente de este tutorial está disponible en GitHub.