Análisis en profundidad de escenarios de uso, principios de implementación e ideas de diseño de ThreadLocal.

Prefacio

ThreadLocal se puede utilizar para almacenar datos locales de subprocesos para aislar los datos de los subprocesos.

El uso inadecuado de ThreadLocal puede provocar pérdidas de memoria. La solución de problemas de pérdidas de memoria no solo requiere estar familiarizado con la JVM y hacer un buen uso de varias herramientas de análisis, sino también un trabajo laborioso.

Si puede comprender su principio y utilizarlo correctamente, no provocará diversos accidentes.

Este artículo analizará ThreadLocal desde los aspectos de escenarios de uso, principios de implementación, pérdidas de memoria, ideas de diseño, etc., y también hablará sobre InheritableThreadLocal.

Escenarios de uso de ThreadLocal

¿Qué es el contexto?

Por ejemplo, cuando un hilo procesa una solicitud, la solicitud pasará por el proceso MVC. Como el proceso es muy largo, pasará por muchos métodos. Estos métodos se pueden llamar contextos.

ThreadLocal se utiliza para almacenar datos de uso común, almacenar información de sesión, almacenar variables locales de subprocesos, etc. en el contexto.

Por ejemplo, utilice un interceptor para obtener la información del usuario que inició sesión a través del token en la solicitud antes de procesar la solicitud y almacene la información del usuario en ThreadLocal, de modo que la información del usuario se pueda obtener directamente de ThreadLocal durante el procesamiento de solicitudes posteriores.

Si el hilo se reutilizará, para evitar confusión de datos, los datos deben eliminarse después de su uso (después de ser procesados ​​por el interceptor)

Los métodos comúnmente utilizados de ThreadLocal son: set(), get(), remove()correspondientes a almacenamiento, adquisición y eliminación respectivamente.

ThreadLocal se puede colocar en una clase de herramienta para facilitar su uso

public class ContextUtils {
    public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
}

Pseudocódigo interceptor

//执行前 存储
public boolean postHandle(HttpServletRequest request)  {
    //解析token获取用户信息
	String token = request.getHeader("token");
	UserInfo userInfo = parseToken(token);   
	//存入
	ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
    
    return true;
}


//执行后 删除
public void postHandle(HttpServletRequest request)  {
    ContextUtils.USER_INFO_THREAD_LOCAL.remove();
}

al usarlo

//提交订单
public void orderSubmit(){
    //获取用户信息
    UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
    //下单
    submit(userInfo);
    //删除购物车
    removeCard(userInfo);
}

Para utilizar mejor ThreadLocal, debemos comprender sus principios de implementación y evitar accidentes causados ​​por un uso inadecuado.

HiloMapa local

Thread Hay dos campos en el hilo que almacenan la clase interna ThreadLocalMap de ThreadLocal.

public class Thread implements Runnable {    
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

threadLocals se utiliza para implementar ThreadLocal

heredableThreadLocals se utiliza para implementar InheritableThreadLocal (el ThreadLocal heredable se analizará más adelante)

imagen.png

La implementación de ThreadLocalMap es una tabla hash, y su clase interna Entry es el nodo de la tabla hash. La tabla hash ThreadLocalMap se implementa mediante la matriz Entry.

public class ThreadLocal<T> {
    //,,,
	static class ThreadLocalMap {
        //...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

La clave en la construcción del nodo es ThreadLocal y el valor es el valor que debe almacenarse.

Al mismo tiempo, el nodo hereda referencias débiles y puede saber a través de genéricos y constructores que establece ThreadLocal como una referencia débil.

Los estudiantes que no comprendan las referencias débiles pueden consultar este artículo: Una introducción detallada a JVM (14) Desbordamientos de memoria, fugas y referencias)

imagen.png

colocar

En el método de almacenamiento de datos.

imagen.png

Obtenga ThreadLocalMap; si no, inicialice ThreadLocalMap (carga diferida)

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
   
    if (map != null) {
        //添加数据
        map.set(this, value);
    } else {
        //没有就初始化
        createMap(t, value);
    }
}

crearMapa

Cree un ThreadLocalMap y asígnelo a los threadLocals del hilo actual.

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

Cree un ThreadLocalMap e inicialice una matriz con una longitud de 16

	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化数组 16
        table = new Entry[INITIAL_CAPACITY];
        //获取下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //构建节点
        table[i] = new Entry(firstKey, firstValue);
        //设置大小
        size = 1;
        //设置负载因子
        setThreshold(INITIAL_CAPACITY);
   }

ThreadLocalMap.set

Obtenga el subíndice mediante hash. Cuando ocurre un conflicto de hash, recorra la tabla hash (ya no use el método de dirección en cadena) hasta que no haya ningún nodo en la posición y luego construya.

Si hay un nodo durante el recorrido, la clave se extraerá de acuerdo con el nodo para comparar. Si es así, se sobrescribirá. Si el nodo no tiene una clave, significa que el ThreadLocal del nodo ha sido reciclado (caducado) Para evitar pérdidas de memoria, el nodo se limpiará.

Finalmente, comprobará si hay nodos caducados en otras ubicaciones, los limpiará y comprobará si hay expansión.

private void set(ThreadLocal<?> key, Object value) {

    //获取哈希表
    Entry[] tab = table;
    int len = tab.length;
    //获取下标
    int i = key.threadLocalHashCode & (len-1);

    //遍历 直到下标上没有节点
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        //获取key
        ThreadLocal<?> k = e.get();
		//key如果存在则覆盖
        if (k == key) {
            e.value = value;
            return;
        }
		//如果key不存在 说明该ThreadLocal以及不再使用(GC回收),需要清理防止内存泄漏
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //构建节点
    tab[i] = new Entry(key, value);
    //计数
    int sz = ++size;
    //清理其他过期的槽,如果满足条件进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

Al obtener el valor hash, use la clase atómica que incrementa el valor hash, y el tamaño del paso es el número de incrementos cada vez (tal vez después de investigar y probar, para minimizar los conflictos de hash)

	//获取哈希值
    private final int threadLocalHashCode = nextHashCode();
    //哈希值自增器
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    //增长步长
    private static final int HASH_INCREMENT = 0x61c88647;

    //获取哈希值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextIndex es para obtener el siguiente índice y vuelve a 0 cuando se excede el límite superior.

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

conseguir

al obtener datos

imagen.png

Obtenga el ThreadLocalMap del hilo actual, inicialícelo si está vacío; de lo contrario, obtenga el nodo

	public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的ThreadLocalMap
        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();
    }

Al obtener el nodo, primero obtenga el subíndice según el valor hash, luego verifique el nodo y compare la clave; si no coincide, significa que la clave ha caducado y puede ocurrir una pérdida de memoria. Es necesario limpiar el hash. mesa.

		private Entry getEntry(ThreadLocal<?> key) {
            //获取下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果匹配 则返回
            if (e != null && e.get() == key)
                return e;
            else
                //匹配不到 去清理
                return getEntryAfterMiss(key, i, e);
        }

pérdida de memoria

Durante el proceso de configuración y obtención de datos, juzgaremos si la clave ha caducado y, si caduca, la limpiaremos.

De hecho, el uso inadecuado de ThreadLocal puede provocar pérdidas de memoria.

Para evitar pérdidas de memoria causadas por un uso inadecuado, los diseñadores intentan limpiar estos ThreadLocals caducados mediante métodos comunes.

Como se mencionó anteriormente, los nodos heredan referencias débiles y establecen la clave para las referencias débiles en la construcción (es decir, ThreadLocal).

Cuando ThreadLocal no se usa en ninguna parte, el siguiente GC establecerá la clave del nodo en vacía

Si el valor ya no se utiliza, pero debido a que la entrada del nodo (nulo, valor) existe, el valor no se puede reciclar, lo que provoca una pérdida de memoria.

imagen.png

Por lo tanto, después de usar los datos, intente usar remove para eliminarlos.

Además, los diseñadores verificarán los nodos con claves vacías y los eliminarán mediante métodos comunes como configurar, obtener y eliminar para evitar pérdidas de memoria.

El pensamiento de diseño

¿Por qué la clave de la entrada, es decir, ThreadLocal, debería establecerse como una referencia débil?

Primero imaginemos un escenario: los subprocesos a menudo se reutilizan en nuestros servicios y, en algunos escenarios, ThreadLocal no se usa durante mucho tiempo.

Si la clave y el valor de la entrada del nodo son referencias fuertes, una vez que ThreadLocal ya no se usa, ThreadLocal aún se almacena en el nodo como una referencia fuerte y no se puede reciclar, lo que equivale a una pérdida de memoria.

Después de configurar ThreadLocal como una referencia débil, aún se producirán pérdidas de memoria si el valor ya no se usa en este escenario. Por lo tanto, en los métodos set, get y remove, los nodos con claves vacías se verificarán y eliminarán para evitar pérdidas de memoria.

Dado que es posible que el valor no se pueda reciclar, ¿por qué no establecerlo como una referencia débil?

Dado que el valor almacena datos aislados de subprocesos, si el valor se establece en una referencia débil, cuando la capa externa no utiliza el objeto correspondiente al valor, no tendrá una referencia fuerte y la próxima vez que se recicle el GC, se producirá la pérdida de datos.

HeredableHiloLocal

InheritableThreadLocal hereda ThreadLocal y se utiliza para transferir variables de subproceso entre subprocesos padre e hijo.

	public void testInheritableThreadLocal(){
        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();

        itl.set("main");

        new Thread(()->{
            //main
            System.out.println(itl.get());
        }).start();
    }

Como se mencionó anteriormente, se usa otro ThreadLocalMap en el hilo para InheritableThreadLocal.

Al crear un hilo, si heredarThreadLocals en el hilo principal no está vacío, se pasa

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //....
    
        //如果父线程中inheritableThreadLocals 不为空 则传递
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
    }

Resumir

ThreadLocal se utiliza para aislar datos entre subprocesos y puede almacenar datos para usarlos en contexto. Dado que los subprocesos se pueden reutilizar, deben eliminarse después de su uso para evitar confusión de datos.

ThreadLocalMap se almacena en el hilo Thread. ThreadLocalMap es una tabla hash que utiliza el método de direccionamiento abierto para resolver conflictos hash. La clave de almacenamiento del nodo es ThreadLocal y el valor almacena los datos que almacenará el hilo.

El nodo hereda la referencia débil y establece ThreadLocal como una referencia débil. Esto hará que ThreadLocal se recicle la próxima vez cuando el GC ya no se use. En este momento, la clave estará vacía. Si el valor ya no se usa, pero el nodo no se elimina, provocará que se utilice un valor, lo que provocará una pérdida de memoria

En los métodos comunes de ThreadLocal como set, get, remove, etc., mientras recorre la matriz, regrese y elimine los nodos caducados (la clave está vacía) para evitar pérdidas de memoria.

Si ThreadLocal se establece en una referencia fuerte, se producirán pérdidas de memoria cuando ThreadLocal ya no se use; cuando ThreadLocal se establece en una referencia débil, aunque también pueden ocurrir pérdidas de memoria, estos datos se pueden verificar y limpiar con métodos comunes; si el valor se establece en una referencia débil, se producirá la pérdida de datos cuando la capa exterior no utilice el valor

InheritableThreadLocal hereda ThreadLocal y se utiliza para la transferencia de datos ThreadLocal entre subprocesos principales e secundarios.

Finalmente (no lo hagas gratis, solo presiona tres veces seguidas para pedir ayuda~)

Este artículo se incluye en la columna " De punto a línea y de línea a superficie" para construir un sistema de conocimiento de programación concurrente Java en términos simples . Los estudiantes interesados ​​​​pueden seguir prestando atención.

Las notas y los casos de este artículo se han incluido en gitee-StudyJava y github-StudyJava . Los estudiantes interesados ​​pueden continuar prestando atención en stat ~

Dirección del caso:

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Si tiene alguna pregunta, puede discutirla en el área de comentarios. Si cree que la escritura de Cai Cai es buena, puede darle me gusta, seguirla y recopilarla para respaldarla ~

Siga a Cai Cai y comparta más información útil, cuenta pública: la cocina privada back-end de Cai Cai

¡Este artículo es publicado por OpenWrite, un blog que publica varios artículos !

Lei Jun: La versión oficial del nuevo sistema operativo de Xiaomi, ThePaper OS, ha sido empaquetada. Una ventana emergente en la página de lotería de la aplicación Gome insulta a su fundador. El gobierno de Estados Unidos restringe la exportación de la GPU NVIDIA H800 a China. La interfaz de Xiaomi ThePaper OS está expuesto. Un maestro usó Scratch para frotar el simulador RISC-V y se ejecutó con éxito. Kernel de Linux Escritorio remoto RustDesk 1.2.3 lanzado, soporte mejorado para Wayland Después de desconectar el receptor USB de Logitech, el kernel de Linux falló Revisión aguda de DHH de "herramientas de empaquetado ": no es necesario construir la interfaz en absoluto (Sin compilación) JetBrains lanza Writerside para crear documentación técnica Herramientas para Node.js 21 lanzadas oficialmente
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10114997
Recomendado
Clasificación