Introducción al Mecanismo de Copia al Escribir
La estrategia de Copia al Escribir (CopyOnWrite, COW) es una técnica de optimización en diseño de software. Su principio fundamental permite que múltiples invocadores compartan un recurso común hasta que uno intente modificarlo. En ese momento, se crea una copia privada para la modificación, mientras los demás siguen accediendo a la versión original. Esto minimiza el uso de recursos cuando predominan las operaciones de lectura.
Implementación Interna de CopyOnWriteArrayList
Esta colección en Java utiliza un bloqueo ReentrantLock y un arreglo volátil. Las operaciones de escritura generan un nuevo arreglo, mientras las lecturas acceden al existente sin bloqueo. A continuación, se muestra un ejemplo simplificado del método add.
public boolean insertar(E elemento) {
final ReentrantLock candado = this.candado;
candado.lock();
try {
Object[] arregloActual = obtenerArreglo();
int dimension = arregloActual.length;
Object[] nuevoArreglo = Arrays.copyOf(arregloActual, dimension + 1);
nuevoArreglo[dimension] = elemento;
establecerArreglo(nuevoArreglo);
return true;
} finally {
candado.unlock();
}
}
Las lecturas no requieren bloqueo, lo que garantiza acceso concurrente eficiente.
public E obtener(int indice) {
return (E) obtenerArreglo()[indice];
}
Extensión a un Mapa Concurrente
El JDK no incluye un CopyOnWriteMap, pero es factible implementar uno basado en los mismos principios. Se utiliza sincronización para escrituras y acceso directo para lecturas.
import java.util.HashMap;
import java.util.Map;
public class MapaCopiaAlEscribir<K, V> implements Map<K, V> {
private volatile Map<K, V> mapaInterno;
public MapaCopiaAlEscribir() {
mapaInterno = new HashMap<>();
}
public V asignar(K clave, V valor) {
synchronized (this) {
Map<K, V> mapaTemporal = new HashMap<>(mapaInterno);
V valorPrevio = mapaTemporal.put(clave, valor);
mapaInterno = mapaTemporal;
return valorPrevio;
}
}
public V obtener(Object clave) {
return mapaInterno.get(clave);
}
}
Operaciones de Modificación en CopyOnWriteArrayList
Las inserciones, eliminaciones y actualizaciones implican duplicar el arreglo interno. El código para añadir en un índice específico sigue este patrón.
public void insertarEn(int posicion, E elemento) {
final ReentrantLock candado = this.candado;
candado.lock();
try {
Object[] arregloBase = obtenerArreglo();
int longitud = arregloBase.length;
Object[] nuevoContenido;
if (posicion == longitud) {
nuevoContenido = Arrays.copyOf(arregloBase, longitud + 1);
} else {
nuevoContenido = new Object[longitud + 1];
System.arraycopy(arregloBase, 0, nuevoContenido, 0, posicion);
System.arraycopy(arregloBase, posicion, nuevoContenido, posicion + 1, longitud - posicion);
}
nuevoContenido[posicion] = elemento;
establecerArreglo(nuevoContenido);
} finally {
candado.unlock();
}
}
Casos de Uso Práctico
Esta colección es ideal para escenarios con predominoi de lecturas, como listas de prohibición en sistemas de búsqueda. Por ejemplo, un servicio que maneja una lista negra actualizada periódicamente.
import java.util.Map;
public class ServicioListaNegra {
private static MapaCopiaAlEscribir<String, Boolean> listaNegra = new MapaCopiaAlEscribir<>();
public static boolean estaEnListaNegra(String id) {
return listaNegra.obtener(id) != null;
}
public static void agregarAMasa(Map<String, Boolean> entradas) {
listaNegra.putAll(entradas);
}
}
Se recomienda inicializar la colección con un tamaño adecuado y utilizar operaciones por lotes para reducir copias innecesarias.
Consideraciones y Limitaciones
El principal inconveniente es el consumo de memoria durante las escrituras, ya que coexisten el arreglo antiguo y el nuevo. Para objetos grandes, esto puede provocar recolecciones de basura frecuentes. Además, la consistencia de datos es eventual, no inmediata, por lo que no es apropiada cuando se requiere lectura instantánea de escrituras recientes. En comparación con Vector, CopyOnWriteArrayList ofrece mejor rendimiento en lecturas al no bloquear en operaciones de consulta.