Comprensión profunda de Netty FastThreadLocal

Autor: Equipo del servidor de Internet vivo-Jiang Zhu


Este artículo toma los extraños problemas en línea como punto de partida, compara la lógica de implementación, las ventajas y desventajas de JDK ThreadLocal y Netty FastThreadLocal, e interpreta el código fuente en profundidad para comprender Netty FastThreadLocal de lo más superficial a lo más profundo.


I. Introducción


He estado aprendiendo sobre Netty recientemente y cuando vi el capítulo de Netty FastThreadLocal, recordé un extraño problema en línea.


Descripción del problema : Cuando la empresa de exportación obtiene información del usuario para determinar si se admite https, la información del usuario obtenida a veces es confusa.


Análisis del problema : cuando se usa ThreadLocal para guardar la información del usuario, la operación remove () no se puede realizar a tiempo. Dado que el subproceso de trabajo de Tomcat se basa en el grupo de subprocesos, se producirá la reutilización de subprocesos, por lo que la información del usuario obtenida puede quedar sobrante del hilo anterior.


Solución del problema : elimine() inmediatamente después de usar ThreadLocal y realice la operación de doble seguro remove() antes de usar ThreadLocal.


A continuación, continúemos profundizando en JDK ThreadLocal y Netty FastThreadLocal.


2. Introducción a JDK ThreadLocal


ThreadLocal es una clase de objeto conveniente proporcionada por JDK que se puede pasar y obtener mediante diferentes métodos en este hilo. Las variables definidas con él solo son visibles en este hilo, no se ven afectadas por otros hilos y están aisladas de otros hilos .


¿Cómo se logra esto? Como se muestra en la Figura 1, cada hilo tendrá una variable de instancia ThreadLocalMap, que se crea mediante carga diferida y se creará cuando el hilo acceda a esta variable por primera vez.


ThreadLocalMap utiliza un método de detección lineal para almacenar objetos ThreadLocal y los datos que mantienen. La lógica de operación específica es la siguiente:

Supongamos que hay un nuevo objeto ThreadLocal y el índice de ubicación donde debe almacenarse se calcula mediante hash como x.


En este momento, se descubre que otros objetos ThreadLocal se han almacenado en la posición correspondiente del subíndice x, luego buscará hacia atrás, con un tamaño de paso de 1, y el subíndice se cambia a x + 1.


A continuación, se encuentra que otros objetos ThreadLocal se han almacenado en la posición correspondiente al subíndice x + 1. De la misma manera, continuará buscando más tarde y el subíndice se cambia a x + 2.


Hasta que se encuentre el subíndice x+3, se considerará libre y luego el objeto ThreadLocal y sus datos mantenidos se construirán en un objeto de entrada y se almacenarán en la ubicación x+3.


Cuando hay muchos datos en ThreadLocalMap, es probable que ocurran conflictos de hash. Para resolver conflictos, se requiere un recorrido descendente continuo. La complejidad temporal de esta operación es O (n) y la eficiencia es baja .



Figura 1


Como se puede ver en el siguiente código:

La clave de entrada es una referencia débil y el valor es una referencia fuerte. Durante la recolección de basura de JVM, siempre que se encuentren objetos con referencias débiles, se reciclarán independientemente de si la memoria es suficiente.


Sin embargo, cuando ThreadLocal ya no se usa y GC lo recicla, la clave de entrada en ThreadLocalMap puede ser NULL. Entonces el valor de entrada siempre tendrá una fuerte referencia a los datos y no se puede liberar. Solo puede esperar el hilo. ser destruido, provocando una pérdida de memoria .

static class ThreadLocalMap {    // 弱引用,在资源紧张的时候可以回收部分不再引用的ThreadLocal变量    static class Entry extends WeakReference<ThreadLocal<?>> {        // 当前ThreadLocal对象所维护的数据        Object value;         Entry(ThreadLocal<?> k, Object v) {            super(k);            value = v;        }    }    // 省略其他代码}

En resumen, dado que ThreadLocal proporcionado por JDK puede tener baja eficiencia y problemas de pérdida de memoria, ¿por qué no realizar la optimización y transformación correspondientes?

1. A juzgar por la anotación de clase ThreadLocal, se introdujo en la versión JDK1.2. Es posible que no se haya prestado mucha atención al rendimiento del programa en los primeros días.


2. En la mayoría de los escenarios de subprocesos múltiples, hay pocas variables ThreadLocal en el subproceso, por lo que la probabilidad de conflictos de hash es relativamente pequeña. Si ocasionalmente ocurren conflictos de hash, el impacto en el rendimiento del programa es relativamente pequeño.


3. Con respecto al problema de pérdida de memoria, el propio ThreadLocal ha tomado ciertas medidas de protección. Como usuario, cuando un objeto ThreadLocal en un hilo ya no se usa o se produce una excepción, llame inmediatamente al método remove() para eliminar el objeto Entry y desarrollar buenos hábitos de codificación.


3. Introducción a Netty FastThreadLocal


FastThreadLocal es una versión optimizada de ThreadLocal proporcionada por JDK en Netty. A juzgar por el nombre, debería ser más rápido que ThreadLocal para hacer frente a escenarios en los que Netty maneja una gran concurrencia y rendimiento de datos.


¿Cómo se logra esto? Como se muestra en la Figura 2, cada hilo tendrá una variable de instancia InternalThreadLocalMap.

Cuando se crea cada instancia de FastThreadLocal, AtomicInteger se utiliza para garantizar un incremento secuencial para generar un índice de subíndice único, que es la ubicación donde se deben almacenar los datos mantenidos por el objeto FastThreadLocal.


Al leer y escribir datos, la ubicación de FastThreadLocal se ubica directamente a través del índice de subíndice de FastThreadLocal, la complejidad del tiempo es O (1) y la eficiencia es alta.


Si el índice de subíndice aumenta a un tamaño muy grande, la matriz mantenida por InternalThreadLocalMap también será muy grande, por lo que FastThreadLocal mejora el rendimiento de lectura y escritura al intercambiar espacio por tiempo.



Figura 2


4. Análisis del código fuente de Netty FastThreadLocal


4.1 Método de construcción

public class FastThreadLocal<V> {    // FastThreadLocal中的index是记录了该它维护的数据应该存储的位置    // InternalThreadLocalMap数组中的下标, 它是在构造函数中确定的    private final int index;     public InternalThreadLocal() {        index = InternalThreadLocalMap.nextVariableIndex();    }    // 省略其他代码}


public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {    // 自增索引, ⽤于计算下次存储到Object数组中的位置    private static final AtomicInteger nextIndex = new AtomicInteger();     private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;     public static int nextVariableIndex() {        int index = nextIndex.getAndIncrement();        if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {            nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);            throw new IllegalStateException("too many thread-local indexed variables");        }        return index;    }    // 省略其他代码}


Los dos fragmentos de código anteriores ya se explicaron en la introducción a Netty FastThreadLocal, por lo que no los repetiremos aquí.


4.2 método de obtención


public class FastThreadLocal<V> {    // FastThreadLocal中的index是记录了该它维护的数据应该存储的位置    private final int index;     public final V get() {        // 获取当前线程的InternalThreadLocalMap        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();        // 根据当前线程的index从InternalThreadLocalMap中获取其绑定的数据        Object v = threadLocalMap.indexedVariable(index);        // 如果获取当前线程绑定的数据不为缺省值UNSET,则直接返回;否则进行初始化        if (v != InternalThreadLocalMap.UNSET) {            return (V) v;        }         return initialize(threadLocalMap);    }    // 省略其他代码}


public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {    private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;     // 未赋值的Object变量(缺省值),当⼀个与线程绑定的值被删除之后,会被设置为UNSET    public static final Object UNSET = new Object();     // 存储绑定到当前线程的数据的数组    private Object[] indexedVariables;     // slowThreadLocalMap为JDK ThreadLocal存储InternalThreadLocalMap    private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =            new ThreadLocal<InternalThreadLocalMap>();     // 从绑定到当前线程的数据的数组中取出index位置的元素    public Object indexedVariable(int index) {        Object[] lookup = indexedVariables;        return index < lookup.length? lookup[index] : UNSET;    }     public static InternalThreadLocalMap get() {        Thread thread = Thread.currentThread();        // 判断当前线程是否是FastThreadLocalThread类型        if (thread instanceof FastThreadLocalThread) {            return fastGet((FastThreadLocalThread) thread);        } else {            return slowGet();        }    }     private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {        // 直接获取当前线程的InternalThreadLocalMap        InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();        // 如果当前线程的InternalThreadLocalMap还未创建,则创建并赋值        if (threadLocalMap == null) {            thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());        }        return threadLocalMap;    }     private static InternalThreadLocalMap slowGet() {        // 使用JDK ThreadLocal获取InternalThreadLocalMap        InternalThreadLocalMap ret = slowThreadLocalMap.get();        if (ret == null) {            ret = new InternalThreadLocalMap();            slowThreadLocalMap.set(ret);        }        return ret;    }     private InternalThreadLocalMap() {        indexedVariables = newIndexedVariableTable();    }     // 初始化一个32位长度的Object数组,并将其元素全部设置为缺省值UNSET    private static Object[] newIndexedVariableTable() {        Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];        Arrays.fill(array, UNSET);        return array;    }    // 省略其他代码}


El método get()  en el código fuente  se divide principalmente en los siguientes tres pasos:

通过InternalThreadLocalMap.get()方法获取当前线程的InternalThreadLocalMap。
根据当前线程的index 从InternalThreadLocalMap中获取其绑定的数据。
如果不是缺省值UNSET,直接返回;如果是缺省值,则执行initialize方法进行初始化。


Sigamos analizando

Lógica de implementación del método InternalThreadLocalMap.get() .


首先判断当前线程是否是FastThreadLocalThread类型,如果是FastThreadLocalThread类型则直接使用fastGet方法获取InternalThreadLocalMap,如果不是FastThreadLocalThread类型则使用slowGet方法获取InternalThreadLocalMap兜底处理。
兜底处理中的slowGet方法会退化成JDK原生的ThreadLocal获取InternalThreadLocalMap。
获取InternalThreadLocalMap时,如果为null,则会直接创建一个InternalThreadLocalMap返回。其创建过过程中初始化一个32位长度的Object数组,并将其元素全部设置为缺省值UNSET。


4.3 método establecido

public class FastThreadLocal<V> {    // FastThreadLocal初始化时variablesToRemoveIndex被赋值为0    private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();     public final void set(V value) {        // 判断value值是否是未赋值的Object变量(缺省值)        if (value != InternalThreadLocalMap.UNSET) {            // 获取当前线程对应的InternalThreadLocalMap            InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();            // 将InternalThreadLocalMap中数据替换为新的value            // 并将FastThreadLocal对象保存到待清理的Set中            setKnownNotUnset(threadLocalMap, value);        } else {            remove();        }    }     private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {        // 将InternalThreadLocalMap中数据替换为新的value        if (threadLocalMap.setIndexedVariable(index, value)) {            // 并将当前的FastThreadLocal对象保存到待清理的Set中            addToVariablesToRemove(threadLocalMap, this);        }    }     private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {        // 取下标index为0的数据,用于存储待清理的FastThreadLocal对象Set集合中        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);        Set<FastThreadLocal<?>> variablesToRemove;        if (v == InternalThreadLocalMap.UNSET || v == null) {            // 下标index为0的数据为空,则创建FastThreadLocal对象Set集合            variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());            // 将InternalThreadLocalMap中下标为0的数据,设置成FastThreadLocal对象Set集合            threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);        } else {            variablesToRemove = (Set<FastThreadLocal<?>>) v;        }        // 将FastThreadLocal对象保存到待清理的Set中        variablesToRemove.add(variable);    }    // 省略其他代码}


public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {    // 未赋值的Object变量(缺省值),当⼀个与线程绑定的值被删除之后,会被设置为UNSET    public static final Object UNSET = new Object();    // 存储绑定到当前线程的数据的数组    private Object[] indexedVariables;    // 绑定到当前线程的数据的数组能再次采用x2扩容的最大量    private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;    private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;     // 将InternalThreadLocalMap中数据替换为新的value    public boolean setIndexedVariable(int index, Object value) {        Object[] lookup = indexedVariables;        if (index < lookup.length) {            Object oldValue = lookup[index];            // 直接将数组 index 位置设置为 value,时间复杂度为 O(1)            lookup[index] = value;            return oldValue == UNSET;        } else { // 绑定到当前线程的数据的数组需要扩容,则扩容数组并数组设置新value            expandIndexedVariableTableAndSet(index, value);            return true;        }    }     private void expandIndexedVariableTableAndSet(int index, Object value) {        Object[] oldArray = indexedVariables;        final int oldCapacity = oldArray.length;        int newCapacity;        // 判断可进行x2方式进行扩容        if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) {            newCapacity = index;            // 位操作,提升扩容效率            newCapacity |= newCapacity >>>  1;            newCapacity |= newCapacity >>>  2;            newCapacity |= newCapacity >>>  4;            newCapacity |= newCapacity >>>  8;            newCapacity |= newCapacity >>> 16;            newCapacity ++;        } else { // 不支持x2方式扩容,则设置绑定到当前线程的数据的数组容量为最大值            newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;        }        // 按扩容后的大小创建新数组,并将老数组数据copy到新数组        Object[] newArray = Arrays.copyOf(oldArray, newCapacity);        // 新数组扩容后的部分赋UNSET缺省值        Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);        // 新数组的index位置替换成新的value        newArray[index] = value;        // 绑定到当前线程的数据的数组用新数组替换        indexedVariables = newArray;    }    // 省略其他代码}


El método set() en el código fuente   se divide principalmente en los siguientes tres pasos:


判断value是否是缺省值UNSET,如果value不等于缺省值,则会通过InternalThreadLocalMap.get()方法获取当前线程的InternalThreadLocalMap,具体实现3.2小节中get()方法已做讲解。
通过FastThreadLocal中的setKnownNotUnset()方法将InternalThreadLocalMap中数据替换为新的value,并将当前的FastThreadLocal对象保存到待清理的Set中。
如果等于缺省值UNSET或nullelse的逻辑),会调用remove()方法,remove()具体见后面的代码分析。


Echemos un vistazo a continuación

Lógica de implementación del método InternalThreadLocalMap.setIndexedVariable .

Determine si el índice excede la longitud de la matriz indexedVariables que almacena datos vinculados al hilo actual. De lo contrario, obtenga los datos en la posición del índice y establezca un nuevo valor para los datos de la posición del índice de la matriz.


Si excede el límite y es necesario expandir la matriz vinculada a los datos del hilo actual, la matriz se expande y los datos en su posición de índice se establecen en un nuevo valor.


La matriz expandida se expande según el índice y la capacidad expandida de la matriz se redondea a la potencia de 2. Luego copie el contenido de la matriz original a la nueva matriz, llene la parte vacía con el valor predeterminado UNSET y finalmente asigne la nueva matriz a indexedVariables.


Sigamos mirando

Lógica de implementación del método FastThreadLocal.addToVariablesToRemove .

1. Obtenga los datos con índice 0 (utilizados para almacenar el conjunto de objetos FastThreadLocal que se va a limpiar). Si los datos tienen el valor predeterminado UNSET o nulo, se creará un conjunto de objetos FastThreadLocal y el conjunto se completará con la posición de la matriz. índice de subíndice 0.


2.如果该数据不是缺省值UNSET,说明Set集合已金被填充,直接强转获取该Set集合。


3.最后将FastThreadLocal对象保存到待清理的Set集合中。


4.4 remove、removeAll方法

public class FastThreadLocal<V> {    // FastThreadLocal初始化时variablesToRemoveIndex被赋值为0    private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();     public final void remove() {        // 获取当前线程的InternalThreadLocalMap        // 删除当前的FastThreadLocal对象及其维护的数据        remove(InternalThreadLocalMap.getIfSet());    }     public final void remove(InternalThreadLocalMap threadLocalMap) {        if (threadLocalMap == null) {            return;        }         // 根据当前线程的index,并将该数组下标index位置对应的值设置为缺省值UNSET        Object v = threadLocalMap.removeIndexedVariable(index);        // 存储待清理的FastThreadLocal对象Set集合中删除当前FastThreadLocal对象        removeFromVariablesToRemove(threadLocalMap, this);         if (v != InternalThreadLocalMap.UNSET) {            try {                // 空方法,用户可以继承实现                onRemoval((V) v);            } catch (Exception e) {                PlatformDependent.throwException(e);            }        }    }     public static void removeAll() {        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();        if (threadLocalMap == null) {            return;        }         try {            // 取下标index为0的数据,用于存储待清理的FastThreadLocal对象Set集合中            Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);            if (v != null && v != InternalThreadLocalMap.UNSET) {                @SuppressWarnings("unchecked")                Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;                // 遍历所有的FastThreadLocal对象并删除它们以及它们维护的数据                FastThreadLocal<?>[] variablesToRemoveArray =                        variablesToRemove.toArray(new FastThreadLocal[0]);                for (FastThreadLocal<?> tlv: variablesToRemoveArray) {                    tlv.remove(threadLocalMap);                }            }        } finally {            // 删除InternalThreadLocalMap中threadLocalMap和slowThreadLocalMap数据            InternalThreadLocalMap.remove();        }    }     private static void removeFromVariablesToRemove(            InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {        // 取下标index为0的数据,用于存储待清理的FastThreadLocal对象Set集合中        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);         if (v == InternalThreadLocalMap.UNSET || v == null) {            return;        }         @SuppressWarnings("unchecked")        // 存储待清理的FastThreadLocal对象Set集合中删除该FastThreadLocal对象        Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;        variablesToRemove.remove(variable);    }     // 省略其他代码}


public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {     // 根据当前线程获取InternalThreadLocalMap       public static InternalThreadLocalMap getIfSet() {        Thread thread = Thread.currentThread();        if (thread instanceof FastThreadLocalThread) {            return ((FastThreadLocalThread) thread).threadLocalMap();        }        return slowThreadLocalMap.get();    }     // 数组下标index位置对应的值设置为缺省值UNSET    public Object removeIndexedVariable(int index) {        Object[] lookup = indexedVariables;        if (index < lookup.length) {            Object v = lookup[index];            lookup[index] = UNSET;            return v;        } else {            return UNSET;        }    }     // 删除threadLocalMap和slowThreadLocalMap数据    public static void remove() {        Thread thread = Thread.currentThread();        if (thread instanceof FastThreadLocalThread) {            ((FastThreadLocalThread) thread).setThreadLocalMap(null);        } else {            slowThreadLocalMap.remove();        }    }    // 省略其他代码}


源码中 remove() 方法主要分为下面2个步骤处理:

通过InternalThreadLocalMap.getIfSet()获取当前线程的InternalThreadLocalMap。具体和3.2小节get()方法里面获取当前线程的InternalThreadLocalMap相似,这里就不再重复介绍了。
删除当前的FastThreadLocal对象及其维护的数据。


源码中 removeAll() 方法主要分为下面3个步骤处理:

通过InternalThreadLocalMap.getIfSet()获取当前线程的InternalThreadLocalMap。
取下标index为0的数据(用于存储待清理的FastThreadLocal对象Set集合),然后遍历所有的FastThreadLocal对象并删除它们以及它们维护的数据。
最后会将InternalThreadLocalMap本身从线程中移除。


五、总结


那么使用ThreadLocal时最佳实践又如何呢?

 每次使用完ThreadLocal实例,在线程运行结束之前的finally代码块中主动调用它的remove()方法,清除Entry中的数据,避免操作不当导致的内存泄漏。


使⽤Netty的FastThreadLocal一定比JDK原生的ThreadLocal更快吗?

不⼀定。当线程是FastThreadLocalThread,则添加、获取FastThreadLocal所维护数据的时间复杂度是 O(1),⽽使⽤ThreadLocal可能存在哈希冲突,相对来说使⽤FastThreadLocal更⾼效。但如果是普通线程则可能更慢。


使⽤FastThreadLocal有哪些优点?

正如文章开头介绍JDK原生ThreadLocal存在的缺点,FastThreadLocal全部优化了,它更⾼效、而且如果使⽤的是FastThreadLocal,它会在任务执⾏完成后主动调⽤removeAll⽅法清除数据,避免潜在的内存泄露。



END

猜你喜欢


本文分享自微信公众号 - vivo互联网技术(vivoVMIC)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

雷军:小米全新操作系统澎湃 OS 正式版已完成封包 国美 App 抽奖页面弹窗辱骂其创始人 美国政府限制向中国出口 NVIDIA H800 GPU 小米澎湃OS界面曝光 大神用 Scratch 手搓 RISC-V 模拟器,成功运行 Linux 内核 RustDesk 远程桌面 1.2.3 发布,增强 Wayland 支持 拔出罗技 USB 接收器后,Linux 内核竟然崩溃了 DHH 锐评“打包工具”:前端根本不需要构建 (No Build) JetBrains 推出 Writerside,创建技术文档的工具 Node.js 21 正式发布
{{o.name}}
{{m.name}}

Supongo que te gusta

Origin my.oschina.net/vivotech/blog/10120430
Recomendado
Clasificación