Introducción a las colecciones Java sincronizadas

1. Información general

El marco de colecciones es un componente clave de Java. Proporciona una gran cantidad de interfaces e implementaciones, lo que nos permite crear y manipular diferentes tipos de colecciones de una manera sencilla.

Aunque el uso de colecciones simples no sincronizadas es simple en general, también puede convertirse en un proceso abrumador y propenso a errores cuando se trabaja en entornos de subprocesos múltiples (también conocido como programación concurrente).

Por lo tanto, la plataforma Java proporciona un fuerte soporte para este escenario a través de diferentes envoltorios de sincronización implementados dentro de la clase Colecciones .

Estos contenedores facilitan la creación de vistas sincronizadas de las colecciones suministradas mediante varios métodos de fábrica estáticos.

En este tutorial, profundizaremos en estos contenedores de sincronización estática. Además, destacaremos la diferencia entre colecciones sincronizadas y colecciones concurrentes .

2. El método synchronizedCollection ()

El primer contenedor de sincronización que cubriremos en este resumen es el método synchronizedCollection () . Como sugiere el nombre, devuelve una colección segura para subprocesos respaldada por la colección especificada .

Ahora, para entender más claramente cómo usar este método, creemos una prueba unitaria básica:

Collection syncCollection = Collections.synchronizedCollection(new ArrayList()); Runnable listOperations = () -> { syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); }; Thread thread1 = new Thread(listOperations); Thread thread2 = new Thread(listOperations); thread1.start(); thread2.start(); thread1.join(); thread2.join(); assertThat(syncCollection.size()).isEqualTo(12); } 

Como se muestra arriba, crear una vista sincronizada de la colección proporcionada con este método es muy simple.

Para demostrar que el método realmente devuelve una colección segura para subprocesos, primero creamos un par de subprocesos.

Después de eso, inyectamos una instancia Runnable en sus constructores, en forma de expresión lambda. Tengamos en cuenta que Runnable es una interfaz funcional, por lo que podemos reemplazarla con una expresión lambda.

Por último, solo verificamos que cada hilo agregue efectivamente seis elementos a la colección sincronizada, por lo que su tamaño final es doce.

3. El método synchronizedList ()

Del mismo modo, de forma similar al método synchronizedCollection () , podemos usar el contenedor synchronizedList () para crear una lista sincronizada .

Como era de esperar, el método devuelve una vista segura para subprocesos de la Lista especificada :

List syncList = Collections.synchronizedList(new ArrayList());

Como era de esperar, el uso del método synchronizedList () parece casi idéntico a su contraparte de nivel superior, synchronizedCollection () .

Por lo tanto, como acabamos de hacer en la prueba unitaria anterior, una vez que hemos creado una lista sincronizada , podemos generar varios hilos. Después de hacer eso, los usaremos para acceder / manipular la Lista de destino de una manera segura para subprocesos.

Además, si queremos iterar sobre una colección sincronizada y evitar resultados inesperados, debemos proporcionar explícitamente nuestra propia implementación segura de subprocesos del bucle. Por lo tanto, podríamos lograr eso usando un bloque sincronizado :

List syncCollection = Collections.synchronizedList(Arrays.asList("a", "b", "c")); List uppercasedCollection = new ArrayList(); Runnable listOperations = () -> { synchronized (syncCollection) { syncCollection.forEach((e) -> { uppercasedCollection.add(e.toUpperCase()); }); } }; 

En todos los casos en los que necesitemos iterar sobre una colección sincronizada, deberíamos implementar este idioma. Esto se debe a que la iteración en una colección sincronizada se realiza a través de múltiples llamadas a la colección. Por lo tanto, deben realizarse como una sola operación atómica.

El uso del bloque sincronizado asegura la atomicidad de la operación .

4. El método synchronizedMap ()

La clase Colecciones implementa otra envoltura de sincronización ordenada, llamada synchronizedMap (). Podríamos usarlo para crear fácilmente un mapa sincronizado .

El método devuelve una vista segura para subprocesos de la implementación del mapa proporcionada :

Map syncMap = Collections.synchronizedMap(new HashMap()); 

5. El método synchronizedSortedMap ()

También hay una implementación equivalente del método synchronizedMap () . Se llama synchronizedSortedMap () , que podemos usar para crear una instancia de SortedMap sincronizada :

Map syncSortedMap = Collections.synchronizedSortedMap(new TreeMap()); 

6. El método synchronizedSet ()

A continuación, avanzando en esta revisión, tenemos el método synchronizedSet () . Como su nombre lo indica, nos permite crear Sets sincronizados con el mínimo esfuerzo.

El contenedor devuelve una colección segura para subprocesos respaldada por el conjunto especificado :

Set syncSet = Collections.synchronizedSet(new HashSet()); 

7. El método synchronizedSortedSet ()

Finalmente, el último contenedor de sincronización que mostraremos aquí es synchronizedSortedSet () .

De manera similar a otras implementaciones de envoltura que hemos revisado hasta ahora, el método devuelve una versión segura para subprocesos del SortedSet dado :

SortedSet syncSortedSet = Collections.synchronizedSortedSet(new TreeSet()); 

8. Colecciones sincronizadas vs concurrentes

Hasta este punto, echamos un vistazo más de cerca a los contenedores de sincronización del marco de colecciones.

Now, let's focus on the differences between synchronized collections and concurrent collections, such as ConcurrentHashMap and BlockingQueue implementations.

8.1. Synchronized Collections

Synchronized collections achieve thread-safety through intrinsic locking, and the entire collections are locked. Intrinsic locking is implemented via synchronized blocks within the wrapped collection's methods.

As we might expect, synchronized collections assure data consistency/integrity in multi-threaded environments. However, they might come with a penalty in performance, as only one single thread can access the collection at a time (a.k.a. synchronized access).

For a detailed guide on how to use synchronized methods and blocks, please check our article on the topic.

8.2. Concurrent Collections

Concurrent collections (e.g. ConcurrentHashMap), achieve thread-safety by dividing their data into segments. In a ConcurrentHashMap, for example, different threads can acquire locks on each segment, so multiple threads can access the Map at the same time (a.k.a. concurrent access).

Concurrent collections are much more performant than synchronized collections, due to the inherent advantages of concurrent thread access.

So, the choice of what type of thread-safe collection to use depends on the requirements of each use case, and it should be evaluated accordingly.

9. Conclusión

En este artículo, analizamos en profundidad el conjunto de contenedores de sincronización implementados dentro de la clase Colecciones .

Además, destacamos las diferencias entre colecciones sincronizadas y concurrentes, y también analizamos los enfoques que implementan para lograr la seguridad de los subprocesos.

Como de costumbre, todos los ejemplos de código que se muestran en este artículo están disponibles en GitHub.