Entendiendo CopyOnWriteArrayList en Java

Entendiendo CopyOnWriteArrayList en Java: Seguridad en la concurrencia



Introducción

En este artículo, vamos a explorar el CopyOnWriteArrayList del paquete java.util.concurrent. Este constructo es sumamente útil en programas multihilo, especialmente cuando necesitamos iterar sobre una lista de manera segura y sin la necesidad de una sincronización explícita.

La concurrencia en Java es un tema fundamental, particularmente en aplicaciones que requieren un alto rendimiento y eficiencia. En este contexto, el CopyOnWriteArrayList se presenta como una herramienta valiosa que permite mantener la integridad de los datos cuando múltiples hilos están involucrados. A lo largo de esta entrada de blog, abordaremos su API, su comportamiento al iterar y proporcionar ejemplos prácticos para entender mejor su funcionalidad.



1. Visión general de CopyOnWriteArrayList

El diseño de CopyOnWriteArrayList utiliza una técnica interesante que permite que sea seguro para hilos sin necesidad de sincronización. Cuando utilizamos cualquier método de modificación, como add() o remove(), todo el contenido del CopyOnWriteArrayList se copia a una nueva instancia interna. Esto implica que podemos iterar sobre la lista de manera segura, incluso cuando se producen modificaciones concurrentes.

El método iterator() nos proporciona un Iterator que se basa en una instantánea inmutable del contenido de la lista en el momento en que se llamó. Esto significa que, a pesar de que otro hilo pueda modificar la lista, nuestra iteración no se verá afectada por esas modificaciones.



Ejemplo de uso

CopyOnWriteArrayList<Integer> lista = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3, 4});
Iterator<Integer> iterador = lista.iterator();
while (iterador.hasNext()) {
    System.out.println(iterador.next());
}


2. API de CopyOnWriteArrayList

La interfaz de CopyOnWriteArrayList es muy similar a la del ArrayList. Sin embargo, existen diferencias importantes en cómo maneja las operaciones en un entorno multihilo. A continuación se presentan algunos de los métodos más utilizados:

  • Constructores: Permite crear una lista vacía o inicializarla con los elementos de otro array o colección.
  • add(E e): Agrega un elemento a la lista, realizando una copia del contenido.
  • remove(Object o): Elimina el primer elemento que coincide con el objeto proporcionado.
  • size(): Devuelve el número de elementos en la lista.
  • iterator(): Devuelve un iterador sobre la lista.

La característica principal que debemos recordar es que cada operación de escritura implica una copia de la lista. Esto puede afectar el rendimiento si realizamos muchas modificaciones, pero a su favor, proporciona una iteración segura.



3. Iterar sobre CopyOnWriteArrayList mientras se insertan elementos

Imaginemos que estamos creando una instancia de CopyOnWriteArrayList que almacena enteros:

CopyOnWriteArrayList<Integer> numeros = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 8});

// Crear un iterador de la lista
Iterator<Integer> iterador = numeros.iterator();

// Agregar un nuevo elemento mientras iteramos
numeros.add(10);

// Al iterar, no veremos el elemento 10
List<Integer> resultado = new LinkedList<>();
iterador.forEachRemaining(resultado::add);

// Aquí solo veremos 1, 3, 5 y 8
assertThat(resultado).containsOnly(1, 3, 5, 8);

Como podemos observar, cuando creamos un iterador para CopyOnWriteArrayList, obtenemos una instantánea inmutable de los datos en la lista en el momento de la creación del iterador. Por lo tanto, aunque hayamos agregado un nuevo número, no aparecerá en nuestra iteración actual.

Para más detalles, si creamos un nuevo iterador después de la adición, veremos el nuevo elemento:

Iterator<Integer> iterador2 = numeros.iterator();
List<Integer> resultado2 = new LinkedList<>();
iterador2.forEachRemaining(resultado2::add);

// Ahora obtendremos 1, 3, 5, 8 y 10
assertThat(resultado2).containsOnly(1, 3, 5, 8, 10);


4. No se permite eliminar mientras se itera

El CopyOnWriteArrayList ofrece la posibilidad de iterar de forma segura sobre los elementos, incluso cuando la lista subyacente se está modificando. Sin embargo, vale la pena mencionar que la operación remove() en el iterador no está permitida. Intentar eliminar un elemento durante la iteración lanzará una UnsupportedOperationException:

@Test(expected = UnsupportedOperationException.class)
public void cuandoIteramosYTratamosDeEliminarUnElementoEntoncesDeberíaLanzarExcepcion() {
    CopyOnWriteArrayList<Integer> numeros = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 8});
    
    Iterator<Integer> iterador = numeros.iterator();
    while (iterador.hasNext()) {
        iterador.remove();  // Esto lanzará UnsupportedOperationException
    }
}

Este comportamiento se debe a que el diseño del CopyOnWriteArrayList se centra en la seguridad durante la iteración, permitiendo que otras operaciones de modificación se efectúen sin bloqueo, pero no permite modificaciones directas a través del iterador.



5. Conclusión

En este tutorial, examinamos la implementación de CopyOnWriteArrayList en el paquete java.util.concurrent. Vimos cómo esta estructura de datos puede ser iterada de manera segura en un entorno multihilo, mientras que otros hilos pueden seguir insertando o eliminando elementos.

Al considerar su uso, debemos tener en cuenta que CopyOnWriteArrayList es particularmente eficaz en escenarios donde la lectura es mucho más frecuente que la escritura. Si tu aplicación implica múltiples modificaciones a la lista, quizás sería mejor explorar otras alternativas como ConcurrentHashMap o synchronizedList.



Consejos prácticos

  • Uso adecuado: Utiliza CopyOnWriteArrayList en situaciones donde las lecturas superen en gran medida a las escrituras.
  • Performance: Realiza pruebas de rendimiento en tus aplicaciones para asegurar que este enfoque no se convierta en un cuello de botella.
  • Alternativas: Familiarízate con otras colecciones concurrentes en Java si sabes que tus operaciones de escritura serán frecuentes.

Utilizando estos enfoques, los programadores de Java pueden maximizar la eficiencia y la seguridad en sus aplicaciones multihilo. Si tienes más preguntas sobre CopyOnWriteArrayList o la programación concurrente en Java, no dudes en dejarlas en los comentarios.