Análisis de código fuente ThreadLocal, ThreadLocalMap

1.Subproceso local

1.1 Descripción general de ThreadLocal

Es una clase de herramienta utilizada para almacenar, obtener y eliminar datos relacionados con subprocesos . Los datos privados reales de subprocesos no se almacenan en este objeto . ThreadLocalHay uno en ThreadLocalMapla clase, que es una tabla implementada con una matriz Hash. Hay una variable miembro de tipo en la Threadclase .ThreadLocalMap

imagen.png

De hecho, al almacenar datos, este ThreadLocalMapobjeto se crea y se asigna a esta propiedad. Los datos almacenados por el método de llamada ThreadLocalen realidad se almacenan aquí. Y este objeto es el objeto interno del subproceso actual, por lo que, de hecho, si desea almacenar datos privados del subproceso, en realidad se almacenan en el objeto del subproceso actual, no en el ThreadLocalobjeto.

1.2 Mapa local de subprocesos

Todos sabemos que ThreadLocalMapes una tabla Hash implementada con una matriz, entonces, ¿cómo se implementa? Echemos un vistazo a la implementación de su estructura de datos subyacente.

1.2.1 Estructura de datos ThreadLocalMap

imagen.png

Una clase ThreadLocalMapse define en Entry, y la encapsulación en esta clase es la suma de la Hashtabla . Podemos ver que en la implementación específica, el objeto se considera y esta referencia ThreadLocal sigue siendo una referencia débil .keyvalueThreadLocalkey

ThreadLocalMapLa capa inferior es construir una Entrymatriz para implementar uno Hash表. La capacidad inicial de esta tabla Hash es 16 .

imagen.png

El umbral de expansión es la longitud de la matriz 2/3.

imagen.png

1) Entonces, ¿por qué ThreadLocalestablecerlo como una referencia débil?

因为如果设置为强引用,当我们不再使用这个ThreadLocal对象时吗,即使我们把栈中的指向ThreadLocal对象的引用设置为null,我们还是不能回收这个对象。因为在堆中的还有一个Entry中的key引用,作为一个强引用指向这个对象。我们会在ThreadLocalMap中维护一个Entry数组来实现Hash表,而ThreadLocalMap对象的引用又在当期线程对象中。所以,在当前线程被销毁前,这部分数据会一直存在,这样就导致了内存泄露。

2)为什么不把value设置为弱引用呢?

ThreadLocal可以设置为弱引用,因为在使用过程中,还是有一个栈中的强引用对象指向堆中的ThreadLocal对象。但是如果把value也设置为弱引用,在栈中却没有一个指向value的强引用。也就是说,value只有弱引用指向,那么只要一发生GC,即使你还在使用ThreadLocal,那么value也会被回收。

1.2.2 ThreadLocalMap代码分析

了解了上面这些知识,那么我们是怎么从这个Hash表中存取数据的呢?来看看它的get、set方法。

1)set方法

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

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
//通过数组长度减1和ThreadLocal对象的hashCode进行与运算得到一个数组下标
int i = key.threadLocalHashCode & (len-1);

//从定位到的下标开始遍历数组,直到数组中的entry = null为止
for (Entry e = tab[i];
     e != null;
     e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    //如果找到key相同的,替换value
    if (k == key) {
        e.value = value;
        return;
    }
	//如果k=null,那么说明这个entry已经是无用数据了
    if (k == null) {
        //将过期的entry替换掉
        replaceStaleEntry(key, value, i);
        return;
    }
}
//如果没有在数组中遍历到,新建一个entry加入到entry = null的位置。
tab[i] = new Entry(key, value);
//数组entry个数加1
int sz = ++size;
/*如果在进行启发式清理的时候没有清理掉过期的entry,并且数组个数已经大于扩容阈值,
那么就进行rehash
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
 }
复制代码
(1)ThreadLocalMap的hashCode

我们看到,ThreadLocalMap是通过获取ThreadLocal的属性threadLocalHashCode再与数组长度减1来得到下标的。那么这个hashCode是怎么实现的?

imagen.png

如上图,一开始nextHashCode是一个原子类对象,值为0。每次获取threadLocalHashCode时都要调用nextHashCode方法,让这个对象的值加上一个固定的hash增量。这个HASH_INCREMENT值是一个特殊的值,他可以让数据在长度为2n的hash表中均匀分布。这样就尽量减少hash冲突

(2)replaceStaleEntry

我们看到,在我们遍历到一个过期的Entry(即k=null的Entry)时,那么我们就要调用replaceStaleEntry方法来替换掉过期的Entry。 `

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    /*
    从过期entry的下标向前遍历,直到entry为null的时候停止,记录找的的最后一个过期的entry
    的下标。
    这个prevIndex方法在到达下标0时会跳到 len-1,所以可能找到的最后一个过期entry在staleSlot
    的后面
    */
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
         slotToExpunge = i;

    /*
    从staleSlot开始往后面找,直到遇到一个空entry
    */
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
	
    
        if (k == key) {
        /*
                如果遇到一个相同的key,将其替换,然后交换当前entry和过期的entry的位置
        */
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
        
            /*
                如果在上一个遍历时没有找到过期entry,那么就将当前的i作为要消除的插槽
                因为当前entry在交换位置有已经是过期entry了。
            */
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            //从当前的过期entry开始探测式清理,再进行启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            //在清理过期数据后,执行结束
            return;
        }
        /*
                如果在向后遍历的过程中遇到一个过期entry,但是前面还有过期的,就表示要从前面开始
                清理,而不是从后面
        */
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    //如果没有找到相同的key,在过期位置新new一个entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    //如果找到了一个新的过期数据,那么就进行清理
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}`
复制代码
(3)为什么要进行交换?

可以看到,我们在碰到一个拥有相同的keyentry的时候,不仅仅进行了值的覆盖,还进行了和过期数据位置的交换,这是为什么?

因为ThreadLocalMap使用的是线性探测法来解决hash碰撞的,如果我们不把他们的位置进行交换,那么就会把这个过期数据清除,这个过期entry就变成的空entry。那么如果下一次插入的entry的key和这次插入的entry的key重复时,那么它就先会通过线性探测法找到这个空的位置,直接插入,那么在hash表中就会存在重复的key了。所以,我们就要对它们的位置进行交换,这是为了保证hash表的key的不重复性的

(4)为什么还要向前找slotToExpunge?

使得后面在进行探测式清理时,探测到尽可能大的范围。

(5)探测式清理

在上面的代码注释中,我们提到了探测性清理,实际上expungeStaleEntry实现的就是探测性清理。 `

 private int expungeStaleEntry(int staleSlot) {
     Entry[] tab = table;
     int len = tab.length;

    // 因为staleSlot位置的entry过期了,所以,把它数据清除
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    //向后遍历知道遇到entry为null
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //如果key为null,清除
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //如果不为null,进行重hash,直到找到一个null的位置插入
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
    
}`
复制代码

其实探测式清除的步骤就是:从过期entry开始向后探测并清除过期entry,并对没有过期的entry进行重新hash,进行位置调整,直到探测到entry为null,停止探测。

1.为什么要进行对遍历的不过期的entry调整位置?

其实这也是为了保证Hash表key的不重复性如果不重新进行位置调整,那么在插入的key重复时,那么就会直接占据前面的为null的位置,后面可能还会存在重复的key

2.为什么都把entry = null作为循环停止条件?

因为使用的是线性探测法,并且还会调整entry的位置来保证key的唯一性,如果entry为null,那就意味着后面没有存在hash碰撞的数据了,存在hash碰撞的entry之间,一定没有null。

(6)启发式清理
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        //i向后遍历
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
     //n = n>>>1,无符号右移,相当与n = n/2。
    } while ( (n >>>= 1) != 0);
    return removed;
}
复制代码

通过源码我们可以看到,启发式清理执行的并不是O(n)时间复杂度的清理,而是O(logn)时间复杂度的清理,从传入的i开始,如果找到一个过期的entry,那么就会调用探测式清理进行清理,并且循环n重新赋值为数组长度

(7)rehash

如果插入数据后启发式清理没有进行并且现在元素个数大于扩容阈值,那么就会进行rehash

imagen.png rehash首先会调用expungeStaleEntries方法,这个方法会将遍历整个hash表,将所有过期的entry全部删除,并把没有过期的数据全部重新hash定位。

imagen.png

如果在清除所有的过期entry后entry个数还是大于扩容阈值的3/4,那么就需要扩容,扩容为原来的2倍,重新设置扩容阈值

(8)set方法总结

通过上面的分析,我们可以总结出set方法的执行流程:

  1. 先通过ThreadLocal的属性threadLocalHashCode与数组长度减1得到数组下标

  2. 从这个数组下标开始探测

    • 如果最先找到一个重复的key,那么就直接替换这个entry的value,返回执行结束。

    • 如果先找到一个过期的entry,那么就替换这个过期的entry,返回执行结束

      • 如果在找到这个过期的entry后,找到重复的key,那么就覆盖value,交换位置。
      • 如果最后还是找到了entry为null的位置,那么就直接new一个entry,将这个entry放在过期entry的地方。
      • 一般设值完成后都会进行探测式清理和启发式清理
    • 如果先遇到一个entry为null的地方,那么就new一个entry,把数据放在这个地方。

      • 如果插入数据后启发式清理没有进行并且现在元素个数大于扩容阈值,那么就会进行rehash
      • 在rehash时会对数组的所有entry都进行扫描,清理所有的过期entry,所有元素重新定位。
      • 如果发现此时entry个数还是大于扩容阈值的3/4,那么就扩容到原来的两倍。

2)get方法

①也是通过threadLocalHashCode变量得到一个下标,看key是否相等。如果key相等,直接返回这个entry

imagen.png

②如果key不相等,那么就从当前的下标开始向后找,直到entry为null。

  1. 如果找到key相等,返回entry
  2. 如果找到过期entry,进行探测式清理。
  3. 如果没有找到,返回null。

imagen.png

3)remove方法

也是通过threadLocalHashCode定位,从这个下标开始遍历,直到entry为null。如果找到目标,将弱引用删除,进行探测式清理。

imagen.png

1.3 ThreadLocal源码

1.3.1get方法

imagen.png

①得到当前线程对象,通过当前线程得到ThreadLocalMap对象。

②Si el mapa no es nulo, ThreadLocalobtenga la entrada a través del objeto actual y obtenga el valor para devolver.

③ Si el mapa es nulo, llame setInitialValueal método y devuelva su valor de retorno.

imagen.png

Almacene un valor inicial a través del initialValuemétodo .ThreadLocal

②Obtenga el objeto de subproceso actual, obtenga el mapa, si el mapa es nulo, cree un nuevo ThreadLocalMapobjeto y asígnelo a la variable en el objeto de subproceso para agregar datos.

③ Si no es nulo, establezca el valor predeterminado.

④Volver al valor predeterminado

1.3.2 establecer método

①Obtenga el objeto de subproceso actual para determinar si existe el mapa

② no existe, crea un mapa, agrega datos

③Existe, agrega datos directamente

imagen.png

1.3.3 método de eliminación

Para determinar si el mapa existe, llame ThreadLocalMapa remove para eliminar los datos si existen.

imagen.png

Supongo que te gusta

Origin juejin.im/post/7079740587347279909
Recomendado
Clasificación