Capítulo 18 Análisis de código fuente local de subprocesos de subprocesos de Java

Los antiguos dijeron

Si no acumula pasos, no puede llegar a mil millas.
Si no hay acumulación de pequeñas corrientes, no hay forma de convertirse en río.
Aunque sentimos que la vida en la era actual se vuelve cada vez más difícil, aunque el mundo está lleno de: "¿Sigues creyendo que mientras trabajes duro, tendrás éxito?".
Desde la perspectiva de la historia, nuestra era actual es solo una gota en el océano. Esas sabidurías que se han acumulado y transmitido a lo largo de miles de años siguen siendo la dirección correcta en la vida.
Hola a todos, soy miembro de los millones de programadores en China, y también soy un programador que nació en la capacitación de TI para estudiantes que no eran computadoras. Cuando estaba confundido y perdido, todavía creía firmemente en la sabiduría transmitida por los antepasados:
Qier Déjalo ir, la madera muerta no se rompe.
La perseverancia, el oro y la piedra se pueden perforar.

Prefacio

Antes de hacer un análisis de un artículo, siempre hago muchos deberes. Este artículo es el mismo. Vi por primera vez ThreadLocal, pero no entendí sus beneficios. Hay muchos artículos en línea con conclusiones aparentemente correctas pero incorrectas, así que planeo resolverlo por mí mismo.
El papel de ThreadLocal es clave en mi comprensión. El hilo puede ser un contenedor de almacenamiento.Cuando necesitamos almacenar cosas en un objeto hilo, necesitamos usar ThreadLocal como clave, y el valor objetivo que debe almacenarse es value. De manera similar a HashMap, el par clave-valor compuesto por el par clave-valor se almacena en el hilo.
En la actualidad, se puede entender que la ventaja es que el aislamiento del hilo, el foco está en el aislamiento de dos palabras. Podemos almacenar los datos de destino en cada hilo para protección de aislamiento y realizar la transferencia de variables entre métodos dentro del alcance del hilo.

estructura

Este capítulo comenzará desde la fuente del diseño, por qué diseñar, cómo diseñar y cuáles son las ventajas y desventajas del esquema de diseño. Cómo lidiar con las desventajas del diseño.
La idea general es: por qué hacerlo (propósito), cómo hacerlo (práctica) y cómo hacerlo mejor (método).

Contenedor en hilo

Actualmente entiendo que si un subproceso tiene su propio contenedor, entonces los datos se pueden aislar y proteger de forma segura, y en un entorno de subprocesos múltiples, no serán interferidos por otros subprocesos.
Por lo tanto, para que el hilo tenga una función de almacenamiento de datos, es inevitable que haya un contenedor de almacenamiento dentro del hilo, ya sea Lista, Mapa, etc. Si elige Lista, obviamente es complicado controlar cuándo se asocian diferentes variables con diferentes tipos de objetos. Y Map puede satisfacer bien esta demanda característica a través de las características clave-valor. Cuando el hilo tiene la función de contenedor, entonces durante el ciclo de vida del hilo, no importa qué método progrese, se puede obtener el Mapa y quitar el valor que contiene.

Inserte la descripción de la imagen aquí
Como se muestra en la figura anterior, cada vez que un subproceso llama a un método, empuja un marco de pila. Estos marcos de pila están en la misma pila que el Mapa. ​​Por lo tanto, independientemente del método al que se ejecute el subproceso, se puede llamar al objeto Mapa Resuelve el problema La transferencia de datos entre los dos aísla los problemas de seguridad de concurrencia en el entorno de múltiples subprocesos.

Cualquiera que haya utilizado HashMap sabe que está compuesto por pares clave-valor. Ahora que hay un mapa en el hilo y queremos almacenar un valor objetivo, ¿cuál es la clave para marcar? El objeto ThreadLocal es para resolver este problema, un objeto ThreadLocal corresponde a un valor.
Entonces, ¿cómo se puede usar? ¿Cuáles son las ventajas en comparación con los métodos tradicionales?

uso

Si desea resolver la transferencia de variables entre métodos, existen dos situaciones:
1. Transferencia de variables entre métodos de la misma clase
2. Transferencia de variables entre métodos de diferentes clases

Para el primer problema, el método tradicional se puede resolver utilizando variables miembro.

Por ejemplo, en la figura anterior, si desea compartir una variable en el método 1 y el método 2, entonces podemos resolverlo definiendo una variable miembro. Asigne el valor en el método 1 y tome el valor en el método 2.

Esto se puede usar si se usa ThreadLocal.
Inserte la descripción de la imagen aquí
Esto también implementa la asignación en el método 1 y el valor en el método 2.

Para la segunda pregunta, si se trata de transferir datos entre métodos en diferentes clases, ¿cómo deben implementarse los métodos tradicionales?

Inserte la descripción de la imagen aquí
En la cadena de llamadas existente, es posible que debamos pasar los parámetros capa por capa hasta que alcancemos el método de destino. Si dices que el uso de variables estáticas estáticas también se puede solucionar, pero sin duda esto producirá problemas de seguridad multiproceso.
¿Cómo lograrlo si usas ThreadLocal?

En este momento, solo necesita obtener la clave threadLocal, y puede obtener los datos almacenados con esta clave en el hilo actual desde otros lugares.

Para ThreadLocal, ¿por qué la recomendación oficial es utilizar decoración privada?
Si se modifica con privado, entonces el alcance de la transferencia de valor en el ciclo de vida de este hilo se limita a esta clase. Para otras clases, es imposible obtener el valor a través de esta clave. En este caso, ¿qué más hay para crear una variable miembro ¿Cuál es la diferencia? Creo que su mayor efecto es que no importa en qué clase se encuentre, siempre que esté dentro del alcance de la llamada al método de pila de subprocesos, se puede obtener.
Entonces, creo que el uso de private proporciona protección a las variables, y para obtener el objeto ThreadLocal, podemos escribir un método get para obtenerlo, y otros métodos de toda la pila de subprocesos pueden obtener el valor correspondiente al obtener el objeto ThreadLocal.

Lo anterior ha dado cuenta del uso y los beneficios de ThreadLocal. El valor de la variable almacenado en el hilo se puede obtener en cualquier lugar, se realiza la transferencia variable entre métodos y se realiza el aislamiento absoluto entre varios hilos.
Analicemos su composición. ¿Qué aspecto tiene este mapa?

ThreadLocal.ThreadLocalMap

ThreadLocalMap es una clase interna de la clase ThreadLoca. Es un contenedor con características de mapa especialmente diseñado para subprocesos. Este ThreadLoca se puede comparar con HashMap.

  1. Su capa inferior se implementa a través de matrices.
  2. Todos están localizados por el algoritmo hash.
  3. Todos tienen las mismas ventajas y desventajas.

La variable miembro ThreadLocal.ThreadLocalMap se mantiene en la clase Thread, por lo que el hilo se da cuenta de la función del contenedor.
Entonces, ¿cuáles son los detalles de implementación de este contenedor?

一 、 ThreadLocalMap

Esta clase es una clase interna de ThreadLocal, pero ThreadLocal no contiene esta variable. Significa que lo produce sin sostenerlo.
Primero observe la composición variable de ThreadLocalMap.

	// 它有一个Entry数组
    private Entry[] table;
	// 数组的初始化容量是16,并且必须是2的幂
	private static final int INITIAL_CAPACITY = 16;
	// 这个变量计量着实际元素个数
    private int size = 0;
	// 和HashMap一样的负载因子,默认为0;
    private int threshold; 

Desde el punto de vista de la composición de variables, es lo mismo que la composición interna de HashMap. Ambos mantienen una matriz de entrada, pero las dos matrices de entrada no son iguales.

static class Entry extends WeakReference<ThreadLocal<?>> {
       
        Object value;

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

Se puede ver en la composición de Entry que usa el objeto ThreadLocal como clave e implementa una referencia débil. Objeto es el valor real que se almacenará.

Referencias débiles y fugas de memoria

Si no está diseñado como una referencia débil, entonces la Entrada del ThreadLocalMap en cada hilo hará una fuerte referencia al objeto ThreadLocal.
Inserte la descripción de la imagen aquí
Como se muestra en la figura anterior, significa que el ciclo de vida de este objeto ThreadLocal es consistente con el ciclo de vida del hilo que hace referencia a él durante más tiempo. A menudo, en el grupo de subprocesos en aplicaciones reales, el subproceso principal será tan largo como el ciclo de la aplicación, lo que hace que el mapa en el subproceso mantenga una referencia al ThreadLocal, y el objeto ThreadLocal no se pueda liberar, lo que provocará una pérdida de memoria. .
Y si está diseñado para ser una referencia débil, no importa cuán largo sea el ciclo de vida del hilo, no mantendrá la referencia al objeto ThreadLocal. El objeto ThreadLocal se reciclará cuando sea el momento de reciclarse durante el fase del ciclo de vida.
Si el objeto ThreadLocal se recicla, las referencias de estos subprocesos serán nulas, pero el objeto ThreadLocal como clave corresponde al valor Value, pero todavía está en el subproceso. Si el subproceso ha estado activo, también es para el valor Value Es fácil provocar pérdidas de memoria. (Pensando que muchos hilos están montados con un Valor inútil, es terrible, ¡es extraño que la memoria no se filtre!)

entonces que debemos hacer?

método remove ()

Si los problemas anteriores pueden causar pérdidas de memoria, entonces este método nació para prevenir pérdidas de memoria. Vea lo que hace.

 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());	// 获取当前线程中挂载的map
     if (m != null)
         m.remove(this);								// 调用ThreadLocalMap 的remove方法
 }

// 下述为m.remove(this)的调用方法
private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;		// 获取map中哈希桶
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);  // 算出当前ThreadLocal对象在这个map中的存放位置
        
        //下方循环中,为了解决哈希冲突,采用的线性查探法。所以定位到的位置上可能不是当前ThreadLocal对象
        对应的Entry。 所以加了判断如果该位置上的ThreadLocal对象内存地址和传入的key相等,才能确定位置。
        for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {			
                e.clear();		
                expungeStaleEntry(i);		// 找到之后调用了这个方法
                return;
            }
        }
    }

// 继续往下看
private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;

        // expunge entry at staleSlot
        tab[staleSlot].value = null;		// 这里释放了value对象。由于key是弱引用会自动收回,所以无需手动释放。
        tab[staleSlot] = null;				// 然后释放了entry结点。 
        size--;
	
	 	// 按理说可以结束了,但是下面它继续在释放。
        // Rehash until we encounter null
        Entry e;
        int i;
        // 这里循环遍历下一个结点,查看它的key是否为null,
        for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {		// 也就是如果下一个key的ThreadLocal对象已经被回收了,那么它的value也应该被回收
                e.value = null;
                tab[i] = null;
                size--;
            } else {
            // 如果下一个结点存在,则判断是否需要做位置校正。这是什么意思呢?
                int h = k.threadLocalHashCode & (len - 1);  
                if (h != i) {		// 如果这个结点计算出来的原定位为h,现在却是了i这个位置,说明这个结点是因为哈希冲突被顺延过来的。现在你前面的位置空出来了,你该回去了。 
                    tab[i] = null;		// 这里就说,这个位置腾出来吧,那腾出来了,这个Entry去哪儿呢 ?

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    // 于是这个entry又从原始位置h出开始重新定位,如果哈希冲突则顺延,不过它肯定不会再回到原来的位置上了。因为它原来位置前方肯定有空着的位置。
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }

Lo que se puede resumir es lo que hace el método de eliminación.

  1. Se libera el objeto de valor vinculado al objeto ThreadLocal actual y se libera el nodo de entrada correspondiente en el mapa.
  2. Compruebe si hay otros fenómenos nulos de ThreadLocal en los nodos subsiguientes en esta posición y libérelo si existe.
  3. Si hay un nodo que se retrasa debido a un conflicto de hash en la posición siguiente, el nodo se reubica.

Por que se recomienda la estática privada

Esta razón proviene de modificar el objeto ThreadLoad con static. La recomendación oficial es que las instancias de threadLocal deben ser estáticas privadas, lo que puede ahorrar el consumo de memoria y evitar la generación repetida de objetos ThreadLocal. Si el objeto ThreadLoca es una variable no estática, significa que la generación de cada objeto externo significa la generación de un ThreadLocal. Thread llama a diferentes objetos externos, el ThreadLocal al que se accede no es el mismo, a menos que el objeto externo sea un singleton. Por supuesto, esto no causará ninguna excepción, pero aumentará la sobrecarga de memoria.
Entonces, si decoramos con static, solo se generará un objeto ThreadLocal, que es suficiente.
Pero esto también tiene desventajas. La variable estática mantiene una fuerte referencia al objeto ThreadLocal, y la variable estática siempre existirá después de que se crea cuando se carga la clase. Esto también hará que la referencia fuerte siempre exista, lo que causará memoria filtraciones, incluso si el hilo El objeto de entrada en el mapa mantiene una referencia débil a él. Qué significa eso?

Inserte la descripción de la imagen aquí
Dado que el uso de estática privada provocará pérdidas de memoria, ¿por qué el funcionario sugiere esto? Si se hace esto, ¿cuál es el punto de referencias débiles en el enfoque oficial similar?
Mi opinión personal es que es seguro que esto provocará una pérdida de memoria, pero solo un objeto está ocupado, lo que puede ahorrar más memoria que generar muchos objetos ThreadLocal. Otro punto es que se puede lograr el mismo efecto sin usar modificaciones estáticas. Por ejemplo, cuando el objeto externo es un singleton, solo hay un objeto ThreadLocal. Entonces, diseñar como una referencia débil todavía tiene su efecto.

método get ()

 public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);		// 获取线程内部的map
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);		// 如果不是null则获取结点
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;		//返回value值
        }
    }
    return setInitialValue();  // 否初始化一个新的map,并创建结点,value初始值为null
}

método set ()

 public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);		// 获取线程内部map
    if (map != null)
        map.set(this, value);		// 在现有map中添加结点
    else
        createMap(t, value);		// 或者初始化一个map
}

Funciones de ThreadLocalMap

(1) Solo la estructura de matriz
ThreadMap mantiene una tabla de matriz, lo que significa que tiene una estructura de matriz. La ubicación del nodo también se basa en el algoritmo Hash para ubicar. A diferencia de HashMap, el nodo de entrada en ThreadLocalMap solo tiene una estructura de valor clave y no hay referencia a los nodos frontal y posterior, por lo que no tiene una estructura de cadena . Entonces, ¿cómo se manejan los conflictos de Hash?

(2)
Detección lineal del método de dirección abierta La detección lineal del método de dirección abierta adoptado por ThreadLocalMap, si ya hay un nodo en la posición calculada por el posicionamiento hash, entonces es bueno posponerlo hacia atrás, si hay más más tarde, luego vuelva a retrasar hasta que se encuentre.

¿No hay un lugar adecuado después de buscarlo?
No, debido a que ThreadLocalMap también tiene un factor de carga, el factor de carga predeterminado es len * 2/3;

private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

Después de la expansión, ¿cómo hash los elementos en el cubo de hash original?

private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;		// 扩容倍数被2倍
        Entry[] newTab = new Entry[newLen];
        int count = 0;

        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC		// 再散列前,先释放无效的结点。
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);	// 用新长度对每个结点重新定位
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

        setThreshold(newLen);
        size = count;
        table = newTab;
    }

para resumir

Este es probablemente el caso de todo ThreadLocal. Si la explicación es incorrecta, por favor dé algunos consejos. Si tienen alguna duda, también pueden discutirlo juntos.

Supongo que te gusta

Origin blog.csdn.net/weixin_43901067/article/details/106504597
Recomendado
Clasificación