1. 三个类之间的关系介绍。
解读ThreadLocal之前,先介绍一下Thread、ThreadLocal、ThreadLocalMap这三个类之间的关系
- ThreadLocal类包含一个静态内部类ThreadLocalMap
- Thread类包含ThreadLocalMap属性threadLocals、inheritableThreadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
三者之间的关系可以用如下类图表示:
2. ThreadLocalMap内部类——Entry
ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap中真正用于存储数据的Entry类,Entry类的定义如下:
/** * 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" in the code that follows. * * 这个hash表的entry继承了WeakReference,使用它的主引用字段作为键(它总是一个ThreadLocal对象)。 * 请注意,null键(即entry.get()== null)表示该键不再被引用,因此可以从表中删除条目。 * 这些条目在下面的代码中被称为"陈旧条目" * * key是ThreadLocal的弱引用,在GC时可能会被回收 */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
3. set、get操作
ThreadLocal.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. * * 将此线程局部变量的当前线程副本设置为指定值。 大多数子类都不需要重写此方法,仅依靠initialValue方法设置线程本地值初始值。 * * @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(); //获取线程t中ThreadLocal.ThreadLocalMap属性threadLocals,ThreadLocal的set方法主要就是通过ThreadLocalMap操作的 ThreadLocalMap map = getMap(t); if (map != null) //map.set(this, value)是因为ThreadLocalMap是将this, value作为一个Entry进行存储 map.set(this, value); else //创建与ThreadLocal关联的ThreadLocalMap createMap(t, value); }
/** * 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. * * 返回此线程局部变量的当前线程副本中的值。 如果变量在当前线程没有值,则返回调用initialValue方法返回的值。 * * @return the current thread's value of this thread-local */ public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取线程t中ThreadLocal.ThreadLocalMap属性threadLocals,ThreadLocal的get方法主要就是通过ThreadLocalMap操作的 ThreadLocalMap map = getMap(t); if (map != null) { //map.getEntry(this)得到ThreadLocalMap.Entry,是因为ThreadLocalMap是以ThreadLocal作为key存储的 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
其中的getMap方法是返回Thread对象中的属性ThreadLocal.ThreadLocalMap threadLocals,其方法定义:
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * 获取与ThreadLocal关联的ThreadLocalMap。 在InheritableThreadLocal中重写。 * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
createMap方法给为了当前Thread类对象初始化ThreadlocalMap属性:
/** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * 创建与ThreadLocal关联的ThreadLocalMap。 在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); }
至此,我们来总结一下ThreadLocal的工作原理:
(1)Thread类中有一个ThreadLocalMap类型的(一个定义在ThreadLocal类中的内部类)成员属性,它是一个Map,他的key是ThreadLocal实例对象。
(2)当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似。
(3)ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
(4)由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的,下面给出一个例子:
public class Son implements Cloneable{ public static void main(String[] args){ Son son=new Son(); System.out.println(son); Thread t = new Thread(() -> { ThreadLocal<Son> threadLocal = new ThreadLocal<>(); System.out.println(threadLocal); threadLocal.set(son); System.out.println(threadLocal.get()); threadLocal.remove(); try { threadLocal.set((Son) son.clone()); System.out.println(threadLocal.get()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } System.out.println(threadLocal); }); t.start(); } }
输出结果:
com.springboot.example.Son@7ef20235
java.lang.ThreadLocal@4b27aea4
com.springboot.example.Son@7ef20235
com.springboot.example.Son@45e059b0
java.lang.ThreadLocal@4b27aea4
也就是如果把一个线程共享的对象直接保存到ThreadLocal中,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 所以要在保存到ThreadLocal之前,通过克隆或者new来创建新的对象,然后再进行保存。
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
4. 一个线程多个ThreadLocal变量间如何区分?
查看ThreadLocal源码:
/** * 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. * * * ThreadLocals(Thread.threadLocals和inheritableThreadLocals)依赖于附加到每个线程 * 的线性探测哈希映射。 ThreadLocal对象充当键,通过threadLocalHashCode进行搜索。 这是一 * 个自定义哈希代码(仅在ThreadLocalMaps中有用),它可以消除通常情况下同一个线程连续构造多个 * ThreadLocals的冲突,同时在不常见的情况下保持良好行为。 * * 对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性, * 对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。 */ 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 into near-optimally spread * multiplicative hash values for power-of-two-sized tables. * * 连续生成的哈希码之间的差值。可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂次方大小的数组中 */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. * * 返回下一个哈希码 * * 它是在上一个被构造出的hashcode/ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647。 * 这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用 * (long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int, * 则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果 * 就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配 * 各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。 */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
(1)对于每一个ThreadLocal对象,都有一个final修饰的int型的threadLocalHashCode不可变属性,对于基本数据类型,可以认为它在初始化后就不可以进行修改,所以可以唯一确定一个ThreadLocal对象。
(2)如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性?
在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次实例化ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次实例化ThreadLocal类,上次被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。这就保证了ThreadLocal类的所有实例化对象的threadLocalHashCode都是不同的。
5. 不同的threadLocalHashCode有什么作用?
这就要看ThreadLocalMap类的set方法是怎么运转的了。
/** * 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. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 线性探测法解决hash冲突的情况 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 找到对应的entry,覆盖entry的value if (k == key) { e.value = value; return; } // key失效,替换失效的entry if (k == null) { replaceStaleEntry(key, value, i); return; } } //找到Entry数组空位,插入新的Entry tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
/** * The table, resized as necessary. * table.length MUST always be a power of two. * * 数组,必要时调整大小。数组长度必须为2的幂 */ private Entry[] table;
因为每个ThreadLocal对象的threadLocalHashCode不同,所以int i = key.threadLocalHashCode & (len-1);就可以尽量避免不同的ThreadLocal对象存储在table数组的相同的位置上。又因为有HASH_INCREMENT=0x61c88647魔数的存在,通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。从而可以充分利用table数组的存储空间。
6. ThreadLocalMap的hash冲突怎么处理
这里就要看到nextIndex方法的源码:
/** * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
可以看到上面的第5小点,ThreadLocalMap在set值的时候,在for循环中调用nextIndex方法,所以ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形来使用的。当访问超过Entry[]边界时,从Entry[]地点位置重新寻找合适的位置存入Entry对象。
7. ThreadLocal的内存泄露问题
重新回顾一下Entry类的源码:
/** * 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" in the code that follows. * * 这个hash表的entry继承了WeakReference,使用它的主引用字段作为键(它总是一个ThreadLocal对象)。 * 请注意,null键(即entry.get()== null)表示该键不再被引用,因此可以从表中删除条目。 * 这些条目在下面的代码中被称为"陈旧条目" * * key是ThreadLocal的弱引用,在GC时可能会被回收 */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry类是将ThreadLocal对象的弱引用作为key来工作的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄露。
ThreadLocalMap设计时的对上面问题的对策:
- 调用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; int i = key.threadLocalHashCode & (len-1); // 线性探测法解决hash冲突的情况 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 找到对应的entry,覆盖entry的value if (k == key) { e.value = value; return; } // key失效,替换失效的entry if (k == null) { replaceStaleEntry(key, value, i); return; } }
调用的replaceStaleEntry方法清理无效的key。
- 调用get方法时的处理策略:
可以看到get方法有个getEntry的操作
private Entry getEntry(ThreadLocal<?> key) { //Entry在table数组中存放的位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else //没找到key对应的entry return getEntryAfterMiss(key, i, e); }
getEntry方法在没有找到key对应的entry时,会调用getEntryAfterMiss方法继续寻找key对应的entry。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //基于线性探测法不断向后探测直到遇到空entry while (e != null) { ThreadLocal<?> k = e.get(); // 找到目标 if (k == key) return e; // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry if (k == null) //删除旧的entry expungeStaleEntry(i); else // 环形意义下往后面走 i = nextIndex(i, len); e = tab[i]; } return null; }
可以看到getEntryAfterMiss方法在发现key为null的情况下,也会调用expungeStaleEntry方法进行清理无效key的操作。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
public void remove() { //获取当前线程相关联的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //删除以ThreadLocal作为key的entry对象 m.remove(this); }
更多有关ThreadLocal类的源码注释请看:
8. 参考文献
- https://www.cnblogs.com/micrari/p/6790229.html
- https://www.cnblogs.com/xzwblog/p/7227509.html#_label0