¿Cómo se implementa ThreadLocal de Java bajo el capó?


Respuestas:


120

Todas las respuestas aquí son correctas, pero un poco decepcionantes, ya que pasan por alto lo inteligente que ThreadLocales la implementación. Estaba mirando el código fuenteThreadLocal y me impresionó gratamente cómo se implementó.

La implementación ingenua

Si te pidiera que implementaras una ThreadLocal<T>clase dada la API descrita en el javadoc, ¿qué harías? Una implementación inicial probablemente sería un ConcurrentHashMap<Thread,T>uso Thread.currentThread()como clave. Esto funcionaría razonablemente bien, pero tiene algunas desventajas.

  • Contención de subprocesos: ConcurrentHashMapes una clase bastante inteligente, pero en última instancia, todavía tiene que lidiar con la prevención de que varios subprocesos se estropeen de alguna manera, y si diferentes subprocesos lo golpean regularmente, habrá ralentizaciones.
  • Mantiene permanentemente un puntero tanto al hilo como al objeto, incluso después de que el hilo haya finalizado y pueda ser GC'ed.

La implementación amigable con GC

Ok, inténtalo de nuevo, solucionemos el problema de la recolección de basura utilizando referencias débiles . Tratar con WeakReferences puede ser confuso, pero debería ser suficiente con usar un mapa construido así:

 Collections.synchronizedMap(new WeakHashMap<Thread, T>())

O si estamos usando Guava (¡y deberíamos hacerlo!):

new MapMaker().weakKeys().makeMap()

Esto significa que una vez que nadie más se aferra al hilo (lo que implica que está terminado), la clave / valor se puede recolectar como basura, lo cual es una mejora, pero aún no aborda el problema de la contención del hilo, lo que significa que hasta ahora nuestro ThreadLocalno es todo eso. increíble de una clase. Además, si alguien decidiera aferrarse a los Threadobjetos después de que hayan terminado, nunca recibirían GC y, por lo tanto, tampoco nuestros objetos, a pesar de que ahora son técnicamente inalcanzables.

La implementación inteligente

Hemos estado pensando en ThreadLocalun mapeo de hilos a valores, pero tal vez esa no sea la forma correcta de pensarlo. En lugar de pensar en ello como un mapeo de Threads a valores en cada objeto ThreadLocal, ¿qué pasaría si lo pensamos como un mapeo de objetos ThreadLocal a valores en cada Thread ? Si cada hilo almacena el mapeo, y ThreadLocal simplemente proporciona una interfaz agradable en ese mapeo, podemos evitar todos los problemas de las implementaciones anteriores.

Una implementación se vería así:

// called for each thread, and updated by the ThreadLocal instance
new WeakHashMap<ThreadLocal,T>()

No hay necesidad de preocuparse por la concurrencia aquí, porque solo un subproceso accederá a este mapa.

Los desarrolladores de Java tienen una gran ventaja sobre nosotros aquí: pueden desarrollar directamente la clase Thread y agregarle campos y operaciones, y eso es exactamente lo que han hecho.

En java.lang.Threadhay las siguientes líneas:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

Lo que, como sugiere el comentario, es de hecho un mapeo privado de paquete de todos los valores que los ThreadLocalobjetos rastrean para esto Thread. La implementación de ThreadLocalMapno es un WeakHashMap, pero sigue el mismo contrato básico, incluida la posesión de sus claves por referencia débil.

ThreadLocal.get() luego se implementa así:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

Y ThreadLocal.setInitialValue()así:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

Esencialmente, use un mapa en este hilo para contener todos nuestros ThreadLocalobjetos. De esta manera, nunca tenemos que preocuparnos por los valores en otros subprocesos ( ThreadLocalliteralmente, solo podemos acceder a los valores en el subproceso actual) y, por lo tanto, no tenemos problemas de concurrencia. Además, una vez que Threadhaya terminado, su mapa será automáticamente GC'ed y todos los objetos locales se limpiarán. Incluso si Threadse sujeta, los ThreadLocalobjetos se sujetan con una referencia débil y se pueden limpiar tan pronto como el ThreadLocalobjeto salga de su alcance.


No hace falta decir que me impresionó bastante esta implementación, que soluciona con bastante elegancia muchos problemas de concurrencia (es cierto, aprovechando ser parte del núcleo de Java, pero eso es perdonable, ya que es una clase tan inteligente) y permite una rápida y acceso seguro para subprocesos a objetos a los que solo es necesario acceder un subproceso a la vez.

tl; ThreadLocal la implementación de dr es bastante buena y mucho más rápida / inteligente de lo que podría pensar a primera vista.

Si le gustó esta respuesta, también puede apreciar mi discusiónThreadLocalRandom (menos detallada) de .

Thread/ ThreadLocalfragmentos de código tomados de la implementación de Oracle / OpenJDK de Java 8 .


1
Tus respuestas se ven increíbles, pero en este momento son demasiado largas para mí. +1 y aceptado, y lo estoy agregando a mi cuenta de getpocket.com para leerlo más tarde. ¡Gracias!
ripper234

Necesito algo similar a ThreadLocal que también me permita acceder a la lista completa de valores, al igual que map.values ​​(). Entonces, mi implementación ingenua es un WeakHashMap <String, Object> donde la clave es Thread.currentThread (). GetName (). Esto evita la referencia al hilo en sí. Si el hilo desaparece, entonces ya nada retiene el nombre del hilo (una suposición, lo admito) y mi valor desaparecerá.
bmauter

De hecho, respondí una pregunta en ese sentido recientemente . A WeakHashMap<String,T>introduce varios problemas, no es seguro para subprocesos y "está diseñado principalmente para usar con objetos clave cuyos métodos iguales prueban la identidad del objeto usando el operador ==", por lo que usar el Threadobjeto como clave sería mejor. Sugeriría usar el mapa de teclas débiles de Guava que describo anteriormente para su caso de uso.
dimo414

1
Bueno, las claves débiles no son necesarias, pero considere usar un ConcurrentHashMap sobre un HashMap sincronizado; el primero está diseñado para acceso multiproceso y funcionará mucho mejor en el caso de que cada subproceso generalmente acceda a una clave diferente.
dimo414

1
@shmosel esta clase está altamente ajustada, por lo que comenzaría con la suposición de que se han hecho tales consideraciones. Un vistazo rápido muestra que cuando un hilo termina normalmente Thread.exit()se llama y verá threadLocals = null;allí mismo. Un comentario hace referencia a este error que también puede disfrutar leyendo.
dimo414

33

Te refieres java.lang.ThreadLocal. Es bastante simple, en realidad, es solo un mapa de pares de nombre-valor almacenados dentro de cada Threadobjeto (ver el Thread.threadLocalscampo). La API oculta ese detalle de implementación, pero eso es más o menos todo lo que hay que hacer.


No veo por qué debería ser necesario, dado que, por definición, los datos solo son visibles para un único hilo.
skaffman

8
Correcto, no hay sincronización ni bloqueo alrededor o dentro de ThreadLocalMap, ya que solo se accede en el hilo.
Cowan

8

Las variables ThreadLocal en Java funcionan accediendo a un HashMap en poder de la instancia Thread.currentThread ().


Eso no es correcto (o al menos ya no lo es). Thread.currentThread () es una llamada nativa en Thread.class. Además, Thread tiene un "ThreadLocalMap" que es un único depósito (matriz) de códigos hash. Este objeto no es compatible con la interfaz del mapa.
user924272

1
Eso es básicamente lo que dije. currentThread () devuelve una instancia de Thread, que contiene un mapa de ThreadLocals a valores.
Chris Vest

4

Suponga que va a implementar ThreadLocal, ¿cómo lo hace específico para un hilo? Por supuesto, el método más simple es crear un campo no estático en la clase Thread, llamémoslo threadLocals. Debido a que cada hilo está representado por una instancia de hilo, threadLocalsen cada hilo también sería diferente. Y esto también es lo que hace Java:

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

¿Qué hay ThreadLocal.ThreadLocalMapaquí? Debido a que solo tiene un threadLocalspara un hilo, si simplemente toma threadLocalscomo su ThreadLocal(digamos, defina threadLocals como Integer), solo tendrá uno ThreadLocalpara un hilo específico. ¿Qué pasa si quieres múltiples ThreadLocalvariables para un hilo? La forma más sencilla es hacer threadLocalsun HashMap, el keyde cada entrada es el nombre de la ThreadLocalvariable y el valuede cada entrada es el valor de la ThreadLocalvariable. ¿Un poco confuso? Digamos que tenemos dos hilos t1y t2. toman la misma Runnableinstancia que el parámetro de Threadconstructor, y ambos tienen dos ThreadLocalvariables llamadas tlAy tlb. Así es como es.

t1.tlA

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     0 |
| tlB |     1 |
+-----+-------+

t2.tlB

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     2 |
| tlB |     3 |
+-----+-------+

Tenga en cuenta que los valores los invento yo.

Ahora parece perfecto. Pero lo que es ThreadLocal.ThreadLocalMap? ¿Por qué no lo usó HashMap? Para resolver el problema, veamos qué sucede cuando establecemos un valor mediante el set(T value)método de la ThreadLocalclase:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

getMap(t)simplemente regresa t.threadLocals. Porque t.threadLocalsfue iniciado a null, entonces ingresamos createMap(t, value)primero:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Crea una nueva ThreadLocalMapinstancia usando la ThreadLocalinstancia actual y el valor que se va a establecer. Veamos cómo ThreadLocalMapes, de hecho es parte de la ThreadLocalclase.

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    ...

    /**
     * Construct a new map initially containing (firstKey, firstValue).
     * ThreadLocalMaps are constructed lazily, so we only create
     * one when we have at least one entry to put in it.
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

    ...

}

La parte central de la ThreadLocalMapclase es Entry class, que se extiende WeakReference. Asegura que si el hilo actual sale, se recolectará la basura automáticamente. Es por eso que usa en ThreadLocalMaplugar de un simple HashMap. Pasa la corriente ThreadLocaly su valor como parámetro de la Entryclase, por lo que cuando queremos obtener el valor, podemos obtenerlo de table, que es una instancia de la Entryclase:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

Así es como se ve en la imagen completa:

La imágen completa


-1

Conceptualmente, puede pensar ThreadLocal<T>que a contiene un Map<Thread,T>que almacena los valores específicos del hilo, aunque no es así como se implementa realmente.

Los valores específicos del hilo se almacenan en el propio objeto Thread; cuando el subproceso termina, los valores específicos del subproceso se pueden recolectar como basura.

Referencia: JCIP


1
Conceptualmente, sí. Pero como puede ver en otras respuestas anteriores, la implementación es completamente diferente.
Archivo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.