一、使用
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<Integer> threadId = * new ThreadLocal<Integer>() { * @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]);
以上述代码为例,其内存布局如下:
其中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那样缩容。因此这也是一个导致线程内存变大的原因。