ThreadLocal源码分析

一、使用

1、使用例子

看如下示例代码,我们有两个线程,a和b,线程a启动之后,sleep 2秒,从threadlocal t1中取获取person实例 p,线程b,启动之后,sleep 1秒,然后set Person的实例p到threadlocal t1中去。

public class ThreadLocalTest {
    volatile static Person p = new  Person();

    static ThreadLocal<Person> t1 = new ThreadLocal<>();


    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" thread a "+t1.get());
        }).start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.set(new Person());
            System.out.println(" thread b "+t1.get());
        }).start();
    }

    static class Person {
        String name;
    }
}

运行结果如下:

thread b com.future.jdk.map.ThreadLocalTest$Person@12b607eb thread a null

可以看到,thread b能获取到p,而thread a不能。这就证明了threadlocal的主要功能。threadlocal提供了一个对线程隔离的局部变量载体。

2、主要功能

先看下API文档的注释

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own(每个线程通过其中的get或者set访问其中的变量都是独立的,不互相影响), independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist(除非有其他对threadLocal的引用依然存在)).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */

3、api

3.1 get

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
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();
}

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

可以看到,ThreadLocal内部维护了一个特殊的HashMap,这个Map存在当前线程(Thread.currentThread())的threadLocals参数中,以当前的ThreadLocal为key。通过当前threadLocal去Map中获取Entry。这个特殊的Map就是ThreadLocalMap。通过getmap方法可以知道,这个Map实际上就维护在Thread对象中。属性为threadLocals。

3.2 set

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely(独自) on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}


/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

通过set方法的源码,我们可以看到,在set的时候,首先判断map是否为null,如果为null则调用creatMap方法,以当前传入的value创建一个以当前ThreadLocal为key的新的map。这个把当前线程的threadLocals 指向这个map。 而InheritableThreadLocal,则会对createMap重写,以实现可继承的在子类中共享的ThreadLocal。 因此可以知道,每个线程都有一个固定的threadLocals属性,这个属性指向一个ThreadLocalMap。

3.3 remove

  /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently(随后)
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method(删除后再次通过get读取时,将会调用intialValue方法再次初始化),
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim(过渡期哦).  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove方法主要是从当前线程的ThreadLocalMap中将ThreadLocal为key的Entry移除。对于Threadlocal,如果使用完毕,则务必调用remove方法移除,以避免引起内存泄漏或者OOM。后面会对这个问题做详细分析。

3.4 withInitial

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

这个withInitial方法是jdk1.8之后专门给lambda方式使用的的构造方法。这个方法采用Lambda方式传入实现了 Supplier 函数接口的参数。如下:

ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 1000);

这样即可用lambda的方式进行调用。

二、核心源码

1、ThreadLocalMap结构

/**
 * ThreadLocalMap is a customized hash map suitable only for(仅适用于)
 * maintaining thread local values. No operations are exported
 * outside of the ThreadLocal class. The class is package private to
 * allow declaration of fields in class Thread.  To help deal with
 * very large and long-lived usages(处理大且生命周期长的value), the hash table entries use
 * WeakReferences for keys(key是弱引用). However, since reference queues are not
 * used, stale entries are guaranteed to be removed only when
 * the table starts running out of space(空间不足,也就是GC)(因为不能使用RerenceQueue,因此旧entry仅在gc时才会被回收).
 */
static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged(删除) from table.  Such entries are referred to
     * as "stale entries" (旧entry)in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

ThreadLocalMap是一个特定的hashMap,只适用于ThreadLocal,private修饰,做为threadLocal的内部类,无法在其他地方访问到。这个ThreadLocalMap的Entry继承了WeakReference,用以实现对value对象的长期缓存。但是,由于用户不能直接操作ReferenceQueue,而WeakReference与Key的绑定,key是ThreadLocal自身,那么Entry到Key之间就是弱引用的关系,因此,只有GC的时候这些过期不用的entry才会被删除。当entry.get()方法为null的时候,表示这个entry是过时的。

2、与WeakReference关系

ThreadLocalMap的Entry中的key是WeakReference的,那么,当对这个key的强引用消失之后,weakReference就会被GC回收。

ThreadLocal a = new ThreadLocal();
a.set(new byte[1024*1024*10]);

以上述代码为例,其内存布局如下:

img

其中value指向10M的堆中数据。如果定义了一个ThreadLocal,那么在Stack上就会有两个指针,分别指向ThreadLocal和当前线程在堆上的内存地址。之后,当前的线程中的threadLocals指向这个ThreadLocalMap,而Map中的Entry,包括Key和Value,Key又通过WeakReference的方式指向了ThreadLocal。Value即是当前需要放在ThreadLocal中的值。可能是一个大的对象,以供线程内部共享。因此value强引用指向了这个value内容。 此时不难发现一个问题,就是当ThreadLocal的强引用一旦消失之后,如申明一个threadLocal变量a,此时令a=null,那么之前的threadlocal就会被GC回收。

ThreadLocal a = new ThreadLocal();
a.set(new byte[1024*1024*10]);
a = null;

此时,如果a=null,那么后面如果执行GC,会导致a被回收,而ThreadLocalMap中,这个a对应的Entry的key就会变成null,而value为10MB,并不会在这次GC中回收。这也是threadLocal可能会造成内存泄漏的原因。因此,如果有threadlocal不需要使用之后,最好的办法是使用remove将其从ThreadLocalMap中移除。

3、ThreadLocalMap核心源码

3.1 Entry

Entry是ThreadLocalMap的核心,也是应用WeakReference的地方。Entry本身继承了WeakReference。之后将传入的ThreadLocal也就是key,放在了WeakReference中,这样构成了对key的WeakReference,而value则是Entry的属性,对value的指针是强引用。

结构如下图

引用关系如下:

3.2 构造函数

ThreadLocal有两个主要的构造函数,分别是创建的时候插入一个Entry和批量插入Entry构造。

3.2.1 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

这个构造函数在使用的时候需要传入第一个key和value。ThreadLoccalMap底层的hash表的长度初始为INITIAL_CAPACITY = 16。 这个构造函数的作用域在protected。

/**
 * The initial capacity -- MUST be a power of two.
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;


/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
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;
    //设置下一次resize的阈值,也就是达到容量的2/3扩容
    setThreshold(INITIAL_CAPACITY);
}

该方法主要配合ThreadLocal中的createMap方法使用。ThreadLocal是采用懒加载的方式,在需要的时候才会创建ThreadLocalMap,由于每个thread都有一个threadlocals来存储对应的ThreadLocalMap,不存在共享问题,因此是线程安全的,不需要加锁。 首先创建INITIAL_CAPACITY大小的Entry数组。之后将firstKey的threadLocalHashCode和(INITIAL_CAPACITY - 1)取模。之后构造一个Entry传入这个hash表计算的index处。然后对于hash表的长度,size是动态计算的,初始为1,后续每次增减会用维护的这个size变量增减。

3.2.2 hreadLocalMap(ThreadLocalMap parentMap)

/**
 * Construct a new map including all Inheritable ThreadLocals
 * from given parent map. Called only by createInheritedMap.
 *
 * @param parentMap the map associated with parent thread.
 */
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            //很显然,key==null的为旧entry,直接忽略就好
            if (key != null) {
              	//如果是threadLocal实例,报错不支持该种操作,如果是InheritableThreadLocal则直接返回e.value,当然也可自定义
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
              	//寻址下一个没有填满的坑位
                while (table[h] != null)
                    //线性探针,直接(h+1)%len
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

批量构造,这种情况发生在InheritableThreadLocal的时候,一个子类要将父类全部的ThreadLocalMap继承,则会使用这个构造函数。除此之外ThreadLocal中不会用到这个构造函数。另外这个构造函数也是private的。不提供给用户访问。仅仅在createInheritedMap方法中调用。

3.3 Hash及hash碰撞的处理方法

在讨论后面的set、get、remove之前,有两个基本的内容需要先理解清楚,第一个内容就是ThreadLocalMap的hash及hash碰撞的解决方法

3.3.1 threadLocalHashCode的计算过程

/**
 * ThreadLocals rely on per-thread linear-probe hash maps attached
 * to each thread (Thread.threadLocals and
 * inheritableThreadLocals).  The ThreadLocal objects act as keys,
 * searched via threadLocalHashCode.  This is a custom hash code
 * (useful only within ThreadLocalMaps) that eliminates collisions
 * in the common case where consecutively constructed ThreadLocals
 * are used by the same threads, while remaining well-behaved in
 * less common cases(在不大常见的情景中保持良好的分布行为).
 */
private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode =
    new AtomicInteger();

/**
 * The difference between successively generated(连续生成的) hash codes - turns
 * implicit sequential thread-local IDs (隐式顺序ID)into near-optimally spread
 * multiplicative(接近最优分布的乘法hash值) hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

3.3.2 hash碰撞的解决办法–开放定址法

虽然使用魔数将hash碰撞的概率降低了很多,但是,hash碰撞的可能性还是存在的。那么出现之后该如何处理呢? 参考前文: 解决哈希冲突的常用方法分析 ThreadLocalMap使用了开放定址法,即从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元。线行探查法是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。

     /**
         * 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);
        }

nextIndex方法即是线性探查寻找下一个元素的方法。同样,prevIndex用来寻找上一个元素。

3.4 Entry过期擦除

此外,还要讨论的第二个问题是,key采用WeakReference,那么被GC回收之后,key将变成null,而value此时还在堆中继续占用内存。因此ThreadLocalMap会在每次set和get线性探测的过程中,将key为null的entry进行擦除。

3.4.1 指定entry的index擦除

/**
 * 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();
        //k==null表示过期,直接擦除
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //重新计算hash值
            int h = k.threadLocalHashCode & (len - 1);
          	//这里的意思是:对于没过期的entry,如果其真正的坑位h(没有经过探针后的地址)不是当前所位于的坑位i,那么找到h对应的最近寻址坑位然后移动到那里即可
          	//此时这里的i对应的entry可能是和staleSlot对应的hash值相同的冲突地址,也可能是另一个hash值,只是其hash值对应的坑位刚好在i的线性探针下,然后中间又没有null entry
            //1、i坑位的hash值计算后还是i,那么不用动,此时该坑位就是正确的
          	//2、i坑位的hash值计算后是h,那么此时的h坑位可能被之前擦除时释放了,所以需要找到其寻址路径下的第一个空坑位填上即可,比如如果其hash值坑位时staleSlot,之前已经被擦除了,直接移动到那里,通过这样,下一个擦除时就不会判断entry为空而导致部分entry没有擦除掉,以及寻址时只需要判断空则代表是否查询成功等。
            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;
}

3.4.2 批量参数cleanSomeSlots

/**
 * 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.
 * (一种不扫描与全扫描的平衡,不扫描很快但是有垃圾,全扫描可能有O(N)数量级)
 
 * @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.
 */
//只要成功移除一个,则返回true
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];
        //找到过期的entry
        if (e != null && e.get() == null) {
          	//n=len,表示如果在log2(N)次之间找到一个过期的,那么再次继续找log2(Length)次
            n = len;
            removed = true;
            //从3.4.1知道,下面的方法会擦除i以及i之后过期的entry,然后返回第一个遇到的空entry
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);//从i往后寻址log2(n)次
    return removed;
}

3.4.3 全量擦除expungeStaleEntries

/**
 * Expunge all stale entries in the table.
 */
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

hash表中的所有元素进行遍历,之后擦除操作。

3.5 setEntry

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be 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.(因为set新entry和替换现有entry一样常见,快速路径通常会失败)

    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)]) {
        ThreadLocal<?> k = e.get();

        //找到相同key直接替换,⚠️此时并没有擦除那些过期的entry哦,这也是内存泄漏的原因呢!!!!
        if (k == key) {
            e.value = value;
            return;
        }

      	//该key过期,直接替换value? TODO
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

  	//找到keyd的hash值对应的第一个空坑位
    tab[i] = new Entry(key, value);
    int sz = ++size;
  	//从i开始寻址log2(sz)次,然后查看是否有过期的entry,如果有直接清除,如果没有且超过了resize阈值,直接resize
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

插入过程中可能会有三种情况,重复key插入;过期插入;常规插入。

  • 重复key插入

    直接替换value

    上图红色部分极为替换之后的value。注意此时是没有擦除3的过期entry的

  • 过期插入

    如果插入过程中,找到的元素其key为null,则说明已过期。

    之后执行插入操作

  • 常规插入

    如果遇到空的位置,能够进行常规插入,那么需要首先进行启发式擦除操作,如果擦除操作中被擦除的元素大于1,则说明插入这个Entry之后不需要扩容。此时直接插入。如果擦除操作没用擦除元素,那么需要执行rehash,判断hash表是否需要扩容。之后再进行插入。

    插入前:

    插入后

3.6 replaceStaleEntry 替换过期Entry

再上一节中用到了一个特殊的方法,如果再插入的过程中遇到了过期的元素,那么需要执行 replaceStaleEntry方法,对过期的元素进行替换。

/**
 * Replace a stale entry encountered during a set operation
 * with an entry for the specified key(替换一个在Set操作中遇到的过期entry).  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.(key对应的hash寻址路径上第一个遇到的过期entry)
 */
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.
  	// 在当前执行过程中往前寻找过期的entry
    // 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).
 		// 一次清除整个run过程中由于GC导致释放了引用,进而过期的entry
  
    //其实这也很好理解
  	//1、如果当前的staleSlot是寻址之后的地址,而key对应的坑位之后已经寻址过但是key不相等的坑位的key现在过期了,那么我们肯定要回到那里去
    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
 		// 如果往前走找到了一个过期的entry,那么slotToExpunge对应的就是往回走遇到的最后一个过期节点的下标
    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.
      	//如果在staleSlot之后遇到一个坑的key等于需要插入的新key,那么需要替换该坑位的entry
        if (k == key) {
            e.value = value;

            //将过期的entry移动到i坑位
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            // 如果往回走的时候没有遇到过期entry,那么就从当前位置开始,否则slotExpunge不变,就从往回走看到的最后一个过期entry开始清除
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 直接从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.
        // staleSlot之后有过期的entry且往回走的时候没有过期entry,后续清除时从i开始
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
  	// 如果staleSlot之后的没有找到key相等的entry,并且遇到了一个空坑位,将节点插入到staleSlot处
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
  	// 如果staleSlot之后有新的过期entry 1,2且往前走没有过期entry,那么从1开始清除,因为将slotToExpunge=i之后,下一个遇到2之后,就不会相等了,而staleSlot又会被插入新的entry,所以从1开始
  	// 当然如果下面的if判断相等的话呢,就代表staleSlot之前之后都没有过期entry,自然不需要清除了
  	// 如果staleSlot之后有新的过期entry 1,2,且往前走的时候存在过期entry,那么从往前走的entry开始清除
    if (slotToExpunge != staleSlot)
        // 清除节点
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

该方法的逻辑是,需要替换的这个位置,通过线性探测查找其上一个位置,一直找到起始位置进行记录,,之后再从staleSlot位置的下一个向后探测。探测分为两种情况。如果遇到key相等的Entry,则直接替换value。如果没有,则在遇到第一个空的entry之后,将新的entry插入到新的staleSlot坑位。 之后需要判断,设置Entry的位置与方法开始传入的staleSlot是否相等,如果不等,则代表往回遍历时有过期节点,从该过期节点开始清除,否则从stale之后遇到的第一个过期节点开始清除。

  • staleSlot之后有重复key

    假定3为目前需要进行replace的位置:

    之后探测到key为1的preIndex的起点,然后向后在4发现了key相等的位置。则直接替换value。

    由于slotToExpunge(1)!= staleSlot(3) 此时执行clean。

  • staleSlot之后没有重复Key

    首先执行探针

    之后在第一个key(假设为6)为null的位置进行替换,然后从slotToExpunge往后开始清除

3.7 get Entry

get Entry的过程中有两种情况。即直接命中和出现碰撞两种情况。

3.7.1 getEntry

/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
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);
}

对命中key的hash计算的位置,判断Entry的key是否相同。如果key相同,则返回。如果不同,则需要用到另外一个方法 getEntryAfterMiss 这也是解决碰撞的方法。

/**
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i]
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
      	// 如果找到直接返回,⚠️但是如果在该entry之前没有遇到过期的entry的话,后面的依然不会擦除
      	// 为了效率
        if (k == key)
            return e;
      	// 如果遇到过期的Entry,往后擦除
        if (k == null)
            expungeStaleEntry(i);
        else
          	// 否则继续探针
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

3.8 remove

/**
 * 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) {
          	//如果找到对应Entry,则清楚,并且清除当前以及之后过期Entry
          	//⚠️但是没有清除当前位置之前的哦
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

3.9 动态扩容

/**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

设置负载因子为2/3。在ThreadLocalMap中,只有添加Entry的set方法才会触发扩容。

 private void set(ThreadLocal<?> key, Object value) {
     tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
 }

在set方法中如果clean没有回收长度,且新加入的Entry会导致长度大于等于threshould触发阈值,则执行rehash方法扩容。

/**
 * Re-pack and/or re-size the table. First scan the entire
 * table removing stale entries. If this doesn't sufficiently
 * shrink the size of the table, double the table size.
 */
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

扩容之前执行expungeStaleEntries,全表扫描清除过期元素。之后再执行resize扩容。 此时再次确认size大于3/4。

/**
 * Double the capacity of the table.
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
 		// 扩容后容量为之前的2倍
    int newLen = oldLen * 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();
          	// 遇到过期的entry直接清除
            if (k == null) {
                e.value = null; // Help the GC
            } else {
              	// 否则插入新的table中,如果有冲突,寻址下一个空的坑位插入
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
		// 新阈值
    setThreshold(newLen);
    size = count;
    table = newTab;
}

以2的倍数进行扩容。 然后将旧的hash表的中的全部元素都按新的hash表进行映射,重新设置值。 再重新设置的过程中,如果遇到key为null则擦除。 LocalThreadMap只有扩容过程,不会收缩。因为一个ThreadLocal的变量应该在一个可控范围。

三、总结

Threadlocal中使用了很多比较巧妙的设计。在此进行总结:

  • ThreadLocal中,threadLocalmap的key是Weakreference的ThreadLocal本身。在强引用消失之后会被GC回收。之后value由于是强引用不会回收,仍然会在内存中。因此这依赖于我们执行threadlocal过程中get和set时的clean操作。但是这个操作不是一定会发生(从上面可以看出,1、get时如果没有冲突或者匹配坑位之前没有遇到过期entry,不会清除匹配坑位之后的entry;2、set时如果是直接替换,替换之前没有遇到过期entry,不会清除坑位之后的过期entry)。因此这也是导致内存泄漏的根源。因此对于threadlocal。我们需要及时使用remove方法将我们不用的对象清除。
  • ThreadLocalMap采用魔数实现hash算法。0x61c88647 这是一个神奇的数字。通过每次加0x61c88647之后取模能尽量均匀的分布在哈希表中。
  • ThreadLocalMap 对于hash冲突采用开放定址法中的线性探测法。每次向后加1。因此这会导致每次get、set、remove、clean等操作都需要进行线性探测。
  • threadLocalMap只能扩容,不会像hashmap那样缩容。因此这也是一个导致线程内存变大的原因。

原文连接:https://cloud.tencent.com/developer/article/1672883

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值