ejemplo - threadlocal java 8
¿Cómo se implementa el ThreadLocal de Java bajo el capó? (5)
¿Cómo se implementa ThreadLocal? ¿Está implementado en Java (usando algún mapa concurrente de ThreadID a Object), o usa algún enlace de JVM para hacerlo de manera más eficiente?
Conceptualmente, puede pensar en un ThreadLocal<T>
como sosteniendo un Map<Thread,T>
que almacena los valores específicos del subproceso, aunque esta no es la forma en que realmente se implementa.
Los valores específicos del subproceso se almacenan en el objeto Thread mismo; cuando el hilo termina, los valores específicos del hilo pueden ser recogidos.
Referencia: JCIP
Las variables ThreadLocal en Java funcionan accediendo a un HashMap mantenido por la instancia Thread.currentThread ().
Supongamos que va a implementar ThreadLocal
, ¿cómo lo hace específico de subprocesos? 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, por lo que threadLocals
en cada hilo también sería diferente. Y esto es también lo que hace Java:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
¿Qué es ThreadLocal.ThreadLocalMap
aquí? Como solo tiene un threadLocals
para un hilo, entonces si simplemente toma threadLocals
como ThreadLocal
(por ejemplo, define threadLocals como Integer
), solo tendrá un ThreadLocal
para un hilo específico. ¿Qué pasa si quieres múltiples variables de ThreadLocal
para un hilo? La forma más simple es hacer que threadLocals
un HashMap
, la key
de cada entrada es el nombre de la variable ThreadLocal
, y el value
de cada entrada es el valor de la variable ThreadLocal
. Un poco confuso? Digamos que tenemos dos hilos, t1
y t2
. toman la misma instancia Runnable
que el parámetro del constructor Thread
, y ambos tienen dos variables tlA
llamadas tlA
y tlb
. Esto es lo que es.
t1.tlA
+-----+-------+
| Key | Value |
+-----+-------+
| tlA | 0 |
| tlB | 1 |
+-----+-------+
t2.tlB
+-----+-------+
| Key | Value |
+-----+-------+
| tlA | 2 |
| tlB | 3 |
+-----+-------+
Tenga en cuenta que los valores están compuestos por mí.
Ahora parece perfecto. Pero, ¿qué es ThreadLocal.ThreadLocalMap
? ¿Por qué no solo usó HashMap
? Para resolver el problema, veamos qué sucede cuando establecemos un valor a través del método set(T value)
de la clase ThreadLocal
:
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 devuelve t.threadLocals
. Como t.threadLocals
se inició a null
, por lo tanto, createMap(t, value)
ingresamos createMap(t, value)
:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Crea una nueva instancia de ThreadLocalMap
utilizando la instancia actual de ThreadLocal
y el valor que se establecerá. Veamos cómo es ThreadLocalMap
, de hecho es parte de la clase ThreadLocal
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 clase ThreadLocalMap
es la Entry class
, que amplía WeakReference
. Asegura que si sale el hilo actual, se recolectará basura automáticamente. Es por eso que usa ThreadLocalMap
lugar de un HashMap
simple. Pasa el ThreadLocal
actual y su valor como el parámetro de la clase Entry
, por lo que cuando queremos obtener el valor, podemos obtenerlo de la table
, que es una instancia de la clase Entry
:
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();
}
Esto es lo que es como en la imagen completa:
Te refieres a java.lang.ThreadLocal
. Es bastante simple, realmente, es solo un Mapa de pares de nombre-valor almacenados dentro de cada objeto Thread
(ver el campo Thread.threadLocals
). La API oculta ese detalle de implementación, pero eso es más o menos todo lo que hay que hacer.
Todas las respuestas aquí son correctas, pero son un poco decepcionantes ya que de alguna manera no ThreadLocal
qué tan inteligente es la implementación de ThreadLocal
. Estaba mirando el código fuente de ThreadLocal
y quedé gratamente impresionado por cómo se implementó.
La implementación ingenua
Si te pedí que implementaras una ThreadLocal<T>
dada la API descrita en el javadoc, ¿qué harías? Una implementación inicial probablemente sería un ConcurrentHashMap<Thread,T>
usando Thread.currentThread()
como su clave. Esto funcionaría razonablemente bien, pero tiene algunas desventajas.
- Discusión de subprocesos:
ConcurrentHashMap
es una clase bastante inteligente, pero en última instancia todavía tiene que ocuparse de evitar que varios subprocesos se burlen de ella de alguna manera, y si los subprocesos diferentes la tocan regularmente, habrá desaceleraciones. - Permanentemente mantiene un puntero al hilo y al objeto, incluso después de que el hilo haya terminado y pueda ser sometido a GC.
La implementación amigable con GC
Vuelve a intentarlo, vamos a tratar el problema de la recolección de basura usando referencias débiles . Tratar con WeakReferences puede ser confuso, pero debería ser suficiente usar un mapa construido de la siguiente manera:
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 subproceso (lo que implica que ha terminado) la clave / valor puede ser basura, lo cual es una mejora, pero aún no soluciona el problema de contención de subprocesos, lo que significa que nuestro ThreadLocal
no es todo esa increíble de una clase. Además, si alguien decidiera aferrarse a los objetos Thread
después de que hubieran terminado, nunca recibirían un GC, y por lo tanto tampoco nuestros objetos, aunque ahora sean técnicamente inalcanzables.
La implementación inteligente
Hemos estado pensando en ThreadLocal
como un 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 consideráramos un mapeo de objetos ThreadLocal a valores en cada Thread ? Si cada hilo almacena la asignación, y ThreadLocal simplemente proporciona una interfaz agradable en esa asignación, 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 hilo accederá a este mapa.
Los desarrolladores 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.Thread
hay 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 del paquete de todos los valores que rastrean los objetos ThreadLocal
para este Thread
. La implementación de ThreadLocalMap
no es un WeakHashMap
, pero sigue el mismo contrato básico, que incluye mantener sus claves por referencia débil.
ThreadLocal.get()
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 objetos ThreadLocal
. De esta forma, nunca debemos preocuparnos por los valores en otros ThreadLocal
( ThreadLocal
literalmente, solo puede acceder a los valores en el subproceso actual) y, por lo tanto, no tenemos problemas de concurrencia. Además, una vez que finalice el Thread
, su mapa se someterá automáticamente a GC y se limpiarán todos los objetos locales. Incluso si el Thread
se mantiene, los objetos ThreadLocal
se mantienen por referencia débil, y se pueden limpiar tan pronto como el objeto ThreadLocal
se salga del alcance.
Huelga decir que quedé bastante impresionado con esta implementación, que con bastante elegancia soluciona muchos problemas de concurrencia (admitiendo que es una ventaja de 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 a subprocesos a objetos a los que solo se debe acceder con un hilo a la vez.
La implementación de tl; dr ThreadLocal
es genial, y mucho más rápida / más inteligente de lo que piensas a primera vista.
Si te gustó esta respuesta, también puedes apreciar mi discusión (menos detallada) de ThreadLocalRandom
.
ThreadLocal
fragmentos de código Thread
/ ThreadLocal
tomados de la implementación de Java 8 de Oracle / OpenJDK .