Análisis del código fuente de ThreadLocal

ThreadLocal permite el acceso seguro a datos en un entorno multihilo. Cada hilo tiene su propia copia de una varible compartida, lo que garantiza la seguridad del hilo.

Ejemplo de uso

Considere el siguiente código:


 public static void main(String[] args) {
        // Inicializa un ThreadLocal con un valor inicial de 100
        final ThreadLocal<integer> MAIN = ThreadLocal.withInitial(() -> 100);

        // Establece un valor en el ThreadLocal del hilo principal
        MAIN.set(200);

        // Crea un nuevo hilo
        new Thread(()->{
            // Intenta obtener el valor del ThreadLocal en el nuevo hilo
            System.out.println(Thread.currentThread().getName() + " MAIN:" + MAIN.get());
        }).start();

        // Obtiene el valor del ThreadLocal en el hilo principal
        System.out.println("MAIN:" + MAIN.get());
        
        // Es crucial llamar a remove() cuando ThreadLocal ya no se necesite para evitar fugas de memoria
        MAIN.remove();

        // Intenta obtener el valor después de removerlo
        System.out.println("MAIN:" + MAIN.get());
    }
</integer>

La salida esperada es:


MAIN:200
Thread-0 MAIN:100

En este ejemplo, aunque el hilo principal establece el valor 200 en MAIN, el nuevo hilo obtiene el valor inicial 100. Esto se debe a que cada hilo tiene su propia instancia de ThreadLocal. La seguridad de ThreadLocal en escenarios multihilo se deriva de este principio. Examinemos el código fuente para comprenderlo mejor.

Propiedades clave

El código fuente de ThreadLocal revela varias propiedades importanets:


    // HashCode único para este ThreadLocal, utilizado para calcular su posición en ThreadLocalMap.
    private final int threadLocalHashCode = nextHashCode();

    // Un AtomicInteger estático asegura que cada ThreadLocal tenga un threadLocalHashCode único en la máquina.
    // Esto es vital para distinguir entre múltiples ThreadLocals dentro del ThreadLocalMap de un hilo.
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    // Constante de incremento para generar hashCodes secuenciales.
    private static final int HASH_INCREMENT = 0x61c88647;

    // Genera el siguiente hashCode para un ThreadLocal.
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

El threadLocalHashCode es un identificador único para cada instancia de ThreadLocal, generado de forma atómica para evitar colisiones en un sistema.

Métodos comunes

Método set()

El método set() maneja la actualización del valor asociado a un ThreadLocal para el hilo actual. Las operaciones de set son seriales dentro de cada hilo, lo que garantiza la seguridad del hilo.


public void set(T value) {
        // Obtiene el hilo actual.
        Thread t = Thread.currentThread();
        // Obtiene el ThreadLocalMap asociado al hilo actual.
        ThreadLocalMap map = getMap(t);
        // Si el mapa existe, establece el valor; de lo contrario, lo inicializa.
        if (map != null)
            map.set(this, value);
        else
            // Inicializa el ThreadLocalMap para el hilo actual.
            createMap(t, value);
    }

// Recupera el ThreadLocalMap del hilo dado.
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// Crea e inicializa un ThreadLocalMap para un hilo.
 void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Método get()

El método get() recupera el valor asociado a un ThreadLocal para el hilo actual. Si no se ha establecido ningún valor, se utiliza el valor inicial proporcionado.


public T get() {
    // Obtiene el hilo actual.
    Thread t = Thread.currentThread();
    // Obtiene el ThreadLocalMap asociado al hilo actual.
    ThreadLocalMap map = getMap(t);
    // Si el mapa existe...
    if (map != null) {
        // Intenta obtener la entrada (Entry) correspondiente a este ThreadLocal.
        ThreadLocalMap.Entry e = map.getEntry(this);
        // Si la entrada existe...
        if (e != null) {
            // Recupera y devuelve el valor almacenado.
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // Si el mapa no existe o la entrada no se encontró, establece el valor inicial.
    return setInitialValue();
}

// Establece el valor inicial si no existe y lo devuelve.
private T setInitialValue() {
    // Obtiene el valor inicial definido por initialValue().
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

// Método por defecto que devuelve null. Puede ser sobrescrito para proporcionar un valor inicial.
protected T initialValue() {
    return null;
}

Método remove()

El método remove() elimina el valor asociado a un ThreadLocal para el hilo actual. Es fundamental llamar a este método para prevenir fugas de memoria cuando el ThreadLocal ya no sea necesario.


    public void remove() {
        // Obtiene el ThreadLocalMap del hilo actual.
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            // Elimina la entrada correspondiente a este ThreadLocal del mapa.
            m.remove(this);
     }

Estos métodos operan sobre una estructura de datos interna llamada ThreadLocalMap, que es la clave para la implementación de ThreadLocal.

ThreadLocalMap

ThreadLocalMap es una clase interna estática de ThreadLocal. Es una implementación de mapa simple, donde las claves son instancias de ThreadLocal y los valores son los datos asociados. Internamente, utiliza un array de Entry.


static class ThreadLocalMap {

    // Entry extiende WeakReference, lo que significa que las claves en el array de Entry son referencias débiles a ThreadLocal.
    // Esto permite que las instancias de ThreadLocal sean recolectadas por el recolector de basura si ya no son referenciadas fuertemente.
    static class Entry extends WeakReference<threadlocal>> {
        // El valor asociado a la clave ThreadLocal.
        Object value;

        // Constructor de Entry.
        Entry(ThreadLocal> k, Object v) {
            super(k); // La referencia débil apunta a la clave ThreadLocal.
            value = v;
        }
    }

    // Capacidad inicial del array de Entry.
    private static final int INITIAL_CAPACITY = 16;

    // El array que almacena las entradas (Entry).
    private Entry[] table;

    // El número actual de entradas en el mapa.
    private int size = 0;

    // Umbral para la redimensionamiento del array.
    private int threshold; // Default to 0

    // Establece el umbral de redimensionamiento.
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

   // Calcula el siguiente índice en el array, con envoltura circular.
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

   // Calcula el índice anterior en el array, con envoltura circular.
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

// Constructor para inicializar el mapa con la primera entrada.
 ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        // Calcula el índice inicial usando el hashCode del ThreadLocal.
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

// ... otros métodos de ThreadLocalMap
}
</threadlocal>

La característica más importante de ThreadLocalMap es que es una propiedad de la clase Thread (threadLocals y inheritableThreadLocals). Esto significa que cada hilo tiene su propia instancia de ThreadLocalMap, lo que garantiza el aislamiento de datos.

Principio de aislamiento de hilos de ThreadLocal

ThreadLocal logra la seguridad y el aislamiento de hilos porque ThreadLocalMap es un atributo del Thread. Como se muestra en el diagrama conceptual (no incluido aquí, pero implícito en la descripción), threadLocals y inheritableThreadLocals son campos de la clase Thread. Cada hilo opera en su propia copia de estos mapas, independientemente de otros hilos.

Cuando se crea un hilo hijo a partir de un hilo padre, los valores de inheritableThreadLocals se copian al hilo hijo. Sin embargo, threadLocals no se copian directamente, lo que permite un aislamiento aún mayor para los datos específicos del hilo.

Método set() en ThreadLocalMap

El método set() dentro de ThreadLocalMap maneja la inserción o actualización de un valor para una clave ThreadLocal dada. Utiliza una estrategia de resolución de colisiones añadiendo 1 al índice si la posición está ocupada.


  private void set(ThreadLocal> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // Calcula el índice inicial basado en el hash del ThreadLocal.
            int i = key.threadLocalHashCode & (len - 1);

            // Estrategia: Itera desde el índice calculado. Si la posición está ocupada,
            // busca la siguiente posición vacía incrementando el índice.
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal> k = e.get(); // Obtiene el ThreadLocal débilmente referenciado.

                // Si se encuentra la misma clave ThreadLocal, actualiza su valor.
                if (k == key) {
                    e.value = value;
                    return;
                }

                // Si la entrada es nula (ThreadLocal recolectado), reemplaza la entrada obsoleta.
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            // Si se encuentra una posición vacía, crea una nueva entrada.
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // Si el tamaño excede el umbral, redimensiona el array.
            // cleanSomeSlots intenta limpiar entradas obsoletas antes de redimensionar.
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash(); // Redimensiona el array si es necesario.
        }

Puntos clave del método set():

  • Se utiliza un AtomicInteger incrementado para generar threadLocalHashCode únicos.
  • Si la posición calculada por el hash ya está ocupada, busca la siguiente posición disponible incrementando el índice (con envoltura circular).

Método getEntry() en ThreadLocalMap

El método getEntry() recupera la entrada (Entry) para una clave ThreadLocal dada. Debido a la estrategia de resolución de colisiones utilizada en set(), getEntry() puede necesitar buscar más allá del índice inicial.


private Entry getEntry(ThreadLocal> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // Comprueba si la entrada en el índice calculado es la correcta.
    if (e != null && e.get() == key)
        return e;
    else
        // Si no es la correcta o es nula, busca en otras posiciones.
        return getEntryAfterMiss(key, i, e);
}

// Busca la entrada después de una falla inicial.
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // Itera a través de las posibles ubicaciones de la clave.
    while (e != null) {
        ThreadLocal> k = e.get();
        if (k == key) // Si se encuentra la clave, devuelve la entrada.
            return e;
        if (k == null) // Si la entrada es obsoleta (ThreadLocal recolectado), la elimina.
            expungeStaleEntry(i);
        else // Calcula el siguiente índice para continuar la búsqueda.
            i = nextIndex(i, len);
        e = tab[i]; // Obtiene la siguiente entrada para la comprobación.
    }
    // Si no se encuentra la clave después de la búsqueda, devuelve null.
    return null;
}

Método resize() en ThreadLocalMap

El método resize() se encarga de expandir el array de Entry cuando el número de elementos alcanza el umbral.


        // Redimensiona el array de Entry duplicando su tamaño.
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2; // Duplica la capacidad.
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            // Copia las entradas existentes al nuevo array.
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal> k = e.get();
                    if (k == null) { // Elimina entradas huérfanas (clave recolectada).
                        e.value = null; // Ayuda al recolector de basura.
                    } else {
                        // Calcula la nueva posición de la clave en el array redimensionado.
                        int h = k.threadLocalHashCode & (newLen - 1);
                        // Maneja colisiones en el nuevo array, buscando la siguiente posición libre.
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e; // Coloca la entrada en la nueva posición.
                        count++;
                    }
                }
            }

            // Actualiza el umbral y el tamaño, y apunta la tabla al nuevo array.
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

El redimensionamiento es seguro para hilos porque ThreadLocalMap es específico de cada hilo, y las operaciones dentro de un solo hilo son inherentemente seriales.

Causa de posibles fugas de memoria con ThreadLocal

Las fugas de memoria pueden ocurrir con ThreadLocal si no se gestionan adecuadamente. Una fuga de memoria ocurre cuando la memoria asignada ya no es necesaria pero no se libera, o cuando un objeto ya no es utilizable pero permanece en memoria.

El ThreadLocalMap.Entry extiende WeakReference<ThreadLocal<?>>. Esto significa que la clave (ThreadLocal) en una entrada es una referencia débil. Cuando una instancia de ThreadLocal ya no tiene referencias fuertes apuntándola (es decir, se ha establecido en null o todas las referencias fuertes han desaparecido), el recolector de basura puede reclamarla. Sin embargo, la entrada en el ThreadLocalMap todavía contiene una referencia fuerte al value.

El problema surge especialmente en entornos con pools de hilos (como servidores de aplicaciones). Un hilo del pool se utiliza para una tarea, y en el proceso, se establece un valor en un ThreadLocal. Si remove() no se llama después de que la tarea finaliza, el hilo del pool, que todavía está vivo y puede ser reutilizado, retiene la referencia al ThreadLocalMap y, por lo tanto, al value asociado. Dado que el hilo sigue vivo, el ThreadLocal original puede ser recolectado (debido a la referencia débil), pero el value permanece en memoria a través del mapa del hilo, hasta que el propio hilo es finalmente destruido (lo cual puede no ocurrir si el pool de hilos se mantiene activo).

Incluso si la clave fuera una referencia fuerte, la fuga seguiría ocurriendo si el ThreadLocal ya no se necesita pero el hilo persiste. Por lo tanto, la llamada a remove() es crucial para limpiar la entrada del ThreadLocalMap y permitir que tanto el ThreadLocal como su valor asociado sean recolectados por el recolector de basura, especialmente en ciclos de vida de hilos largos o reutilizados.

Etiquetas: java multihilo ThreadLocal Gestión de memoria Recolección de basura

Publicado el 10-31 18:43