¿Cómo se implementa ThreadLocal?

¿Cómo se implementa ThreadLocal?

Todos están familiarizados con ThreadLocal, entonces, ¿cómo funciona?
Echemos un vistazo a su mecanismo de implementación de acuerdo con nuestro orden de uso habitual (tenga en cuenta que el código fuente solo proporciona el contenido necesario)

Código versión jdk8

  • ThreadLocal ()
  • Establecer () puntos clave, todos deberían leer la publicación cuidadosamente
  • obtener()
  • eliminar()

Hilo Local

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

Hey Nada!
Sorpresa o sorpresa? Inesperado?

método set ()

La lógica principal del método central ThreadLocal está aquí

    public void set(T value) {
        // 拿到当前线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 直觉告诉我这里不简单
            map.set(this, value);
        else
            // 直觉告诉我这里不简单
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Hay algunos puntos en el contenido que pueden ser más. Entraremos en orden.

  • ThreadLocal.ThreadLocalMap
  • ThreadLocal.createMap ()
  • ThreadLocal.ThreadLocalMap.set ()

ThreadLocal.ThreadLocalMap

Thread.threadLocals es una variable miembro dentro del hilo, la clase interna ThreadLocalMap de ThreadLocal es un contenedor de mapas.

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    static class ThreadLocalMap {
        
        // 内部数组初始长度
        private static final int INITIAL_CAPACITY = 16;
        
        // 又是一个内部类,用于存储具体数据
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            /**
            *
            *
            */
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        
        // 数组用于存储ThreadLocal 和 value的对应关系
        private Entry[] table;
        // 数组table 扩容的 阈值
        private int threshold; // Default to 0
                
        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);
        }
        
        // 设置数组扩容阈值,初始化和扩容后都会调用这个方法
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
    }

La
clase ThreadLocalMap.Entry usa WeakReference (referencia débil) para almacenar la clave (ThreadLocal)
porque la referencia débil no evitará la devolución de basura, evitando que el GC no pueda reciclar ThreadLocal si las referencias de clase de hilo al ThreadLocal hacen que el hilo sobreviva .
Esto causará confusión en el mecanismo de reutilización de subprocesos del grupo de subprocesos, y es más probable que las operaciones incorrectas causen desbordamiento de memoria.
La referencia débil resuelve el problema de la recolección de basura, pero trae otro problema, los desarrolladores de jdk necesitan mantener la clave caducada en la matriz, veremos esta parte del código más adelante

ThreadLocal.ThreadLocalMap.set ()

// 注意这里只写了有关 ThreadLocal.ThreadLocalMap.set() 的源码 不是全部源码
static class ThreadLocalMap {
    
    // 看过hashmap等 map类实现的都知道,看map容器先看hash
    private final int threadLocalHashCode = nextHashCode();
    
    /**
     * BigDecimal goldenRatioResult = BigDecimal.valueOf(Math.pow(2, 32)).divide(new BigDecimal("1.618"), 0, ROUND_HALF_UP);
     * int hashIncrenment = ~goldenRatioResult.intValue() + 0b1; // 等效于 Math.abs() 结果是 1640531527 也就是十六进制的 0x61c88647
     * 1.618 是 1:0.618,是神奇的黄金分割数。
     * HASH_INCREMENT是根据黄金分隔数计算出来的一个值,使threadLocalHashCode的值之间被HASH_INCREMEN分隔
     * 旨在用这样的hash值生成更均匀的数组下标, 并减少冲突概率
     * 有一篇帖子是专门讲 为什么用0x61c88647这个数的,我就不献丑了文底贴链接
     *
     *
     * 看不懂?没关系只需要记住用这个数作为间隔,生成的hash值计算出来的数组下标更均, 并且冲突几率小
     * 后面称这个数为魔数
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 以HASH_INCREMENT 递增生成hash
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            // len 数组的长度被控制为2的整数倍, len-1 的二进制为 1111111**11,这种有固定规律的格式,
            // 方便通过位运算生成数组下标时排除自身因素造成的冲突
            int len = tab.length;
            // 通过hash值位运算计算出 数组下标
            int i = key.threadLocalHashCode & (len-1);

            // 这里与hashmap不同, 因为ThreadLocal 的特性在合理的架构设计下是不会大规模使用的。
            // 又因为有魔数HASH_INCREMENT = 0x61c88647; 作为分隔。
            // 所以hash取下标的操作 发生冲突的可能性很小,且分布有一定间隔,所以这里干脆用循环查找可用节点的方式解决冲突
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 循环检查
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    // 节点为当前 ThreadLocal 直接替换value
                    e.value = value;
                    return;
                }

                if (k == null) {
                    // 过期的数据, 因为key使用的弱引用gc回收之后就是空值, 需要维护清理
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // e 等于空直接使用

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 没有成功删除任何过时的节点, 并且当前集合内有效数据长度达到扩容阈值 去扩容
                rehash();
        }
}

Aquí aparecen dos funciones más

  • replaceStaleEntry para borrar datos obsoletos
  • cleanSomeSlots también limpia datos obsoletos y devuelve si más de un espacio se limpió con éxito
  • repetir actualización y expansión

En este punto, la parte nutritiva de la función set ha terminado. ReemplazarStaleEntry cleanSomeSlots y otras funciones se encapsulan para limpiar la matriz de reordenamiento de datos.
¿Por qué limpiar la matriz reorganizada? Debido a que el elemento de la matriz es Entrada, utiliza referencias débiles para almacenar claves

La publicación sobre el código fuente relevante no se introduce en detalle

/**
 * Replace a stale entry encountered during a set operation
 * with an entry for the specified key.  The value passed in
 * the value parameter is stored in the entry, whether or not
 * an entry already exists for the specified key.
 *
 * As a side effect, this method expunges all stale entries in the
 * "run" containing the stale entry.  (A run is a sequence of entries
 * between two null slots.)
 *
 * @param  key the key
 * @param  value the value to be associated with key
 * @param  staleSlot index of the first stale entry encountered while
 *         searching for key.
 */
 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    // 找到最靠前的过期数据一次性清理干净
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            // 发现了当前ThreadLocal 存储在了其他位置 下面进行校准替换
            // 一定是有过期数据没有清理造成的
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                // 与备份位置相同 即没有找到过期的节点
                // 即从当前位置开始清理
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

/**
 * Heuristically scan some cells looking for stale entries.
 * This is invoked when either a new element is added, or
 * another stale one has been expunged. It performs a
 * logarithmic number of scans, as a balance between no
 * scanning (fast but retains garbage) and a number of scans
 * proportional to number of elements, that would find all
 * garbage but would cause some insertions to take O(n) time.
 *
 * @param i a position known NOT to hold a stale entry. The
 * scan starts at the element after i.
 *
 * @param n scan control: {@code log2(n)} cells are scanned,
 * unless a stale entry is found, in which case
 * {@code log2(table.length)-1} additional cells are scanned.
 * When called from insertions, this parameter is the number
 * of elements, but when from replaceStaleEntry, it is the
 * table length. (Note: all this could be changed to be either
 * more or less aggressive by weighting n instead of just
 * using straight log n. But this version is simple, fast, and
 * seems to work well.)
 *
 * @return true if any stale entries have been removed.
 */
private boolean cleanSomeSlots(int i, int n) {
    // 试探性的扫描一些元素,对过期节点进行清理
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}


/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null 刷新条目
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            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;
}

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

el método get es relativamente simple

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();
}


// ThreadLocalMap map 没初始化 就先进行初始化
// 初始化结束不过瘾, 就先value设置个 null. (真皮)
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;
}

protected T initialValue() {
    return null;
}


// map.getEntry 在这里
private Entry getEntry(ThreadLocal<?> key) {
    // hsah值计算下标 上面已经见识过了
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 是我的 value 直接返回
        return e;
    else
        // 遇到了冲突 调用封装方法查找
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            // 找到了对应节点
            return e;
        if (k == null)
            // 清理过期节点
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}


Bueno, vemos que el método get también inicializará el mapa. Cuando se encuentra con una colisión de hash, se ciclará e incrementará hasta encontrar nulo (lo que representa que no se encuentra ningún valor), o encontrar un elemento que almacena la misma clave.

eliminar es más simple

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

/**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            // 清除弱引用
            e.clear();
            // 清除节点
            expungeStaleEntry(i);
            return;
        }
    }
}

El final

Una gran parte del contenido de este artículo necesita un poco de comprensión del principio de la estructura del mapa para que se comprenda mejor, incluida la operación de bit hash collision bit binary. Los amigos confundidos no me prestan mucha atención. Esta semana publicaré el próximo artículo para analizar el código fuente de hashMap , que explicará en detalle el principio de hashmap.

(⊙ o ⊙) ¡Ah!

Sobre número mágico

17 artículos originales publicados · ganó 24 · vistas 280,000 +

Supongo que te gusta

Origin blog.csdn.net/qq_22956867/article/details/99759473
Recomendado
Clasificación