一、底层结构
ThreadLocal底层有一个默认容量为16的数组组成,k是ThreadLocal对象的引用,v是要放到TheadLocal的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
数组类似为 HashMap,对哈希冲突的处理不是用链表/红黑树处理,而是使用链地址法,即尝试顺序放到哈希冲突下标的下一个下标位置
该数组也可以进行扩容
二、工作原理
一个 ThreadLocal 对象维护一个 ThreadLocalMap 内部类对象,ThreadLocalMap 对象才是存储键值的地方
更准确的说,是 ThreadLocalMap 的 Entry 内部类是存储键值的地方
见源码 set(),createMap() 可知
因为一个 Thread 对象维护了一个 ThreadLocal.ThreadLocalMap 成员变量,且 ThreadLocal 设置值时,获取的 ThreadLocalMap 正是当前线程对象的 ThreadLocalMap
// 获取 ThreadLocalMap 源码
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
所以每个线程对 ThreadLocal 的操作互不干扰,即 ThreadLocal 能实现线程隔离
三、使用
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Java");
String i = threadLocal.get()
// i = Java
四、为什么 ThreadLocal.ThreadLocalMap 底层是长度 16 的数组?
对ThreadLocal的操作见第三点,可以看到ThreadLocal每次set方法都是对同个key(因为是同个ThreadLocal对象,所以key肯定都是一样的)进行操作
如此操作,看似对ThreadLocal的操作永远只会存1个值,那用长度为1的数组它不香吗?为什么还要用16长度呢?
好了,其实这里有个需要注意的地方,ThreadLocal是可以存多个值的,代码如下:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("Java2");
按代码执行后,看着是new了2个ThreadLocal对象,但实际上,数据的存储都是在同一个ThreadLocal.ThreadLocalMap 上操作的
再次强调:ThreadLocal.ThreadLocalMap 才是数据存取的地方,ThreadLocal只是api调用入口,真相在ThreadLocal类源码的getMap()
因此上述代码最终结果就是一个ThreadLocalMap存了2个不同ThreadLocal对象作为key,对应value为Java,Java2
我们在看下ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
// 这里每次 set 之前,都会调用 getMap(t) 方法,t 是当前调用 set 方法的线程
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 重点:返回调用 set 方法的线程(例子是主线程)的 ThreadLocal 对象。
// 所以不管 api 调用方 new 多少个 ThreadLocal 对象,它永远都是返回调用线程(例子是主线程)的 ThreadLocal.ThreadLocalMap 对象供调用线程去存取数据。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// t.threadLocals 的声明如下
ThreadLocal.ThreadLocalMap threadLocals = null;
// 仅有一个构造方法
public ThreadLocal() {
}
五、数据存放在数组中,如何解决hash冲突问题
使用链地址法解决
具体怎么解决?看看执行get、set方法的时候:
- set:
·1.数组的key等于该ThreadLocal,则覆盖该位置元素
·2.否则就找下一个空位置,直到找到空或者key相等为止
·3.根据ThreadLocal对象的hash值,定位到ThreadLocalMap数组中的位置
·4.如何位置无元素而直接放到该位置
·5.如果有元素,则覆盖该位置元素 - get:
·1.根据ThreadLocal对象的hash值,定位到ThreadLocalMap数组中的位置
·2.如果不一致,就判断下一个位置
·3.否则则直接取出
// 数组元素结构
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
六、ThreadLocal的内存泄露隐患
三个前置知识:
- ThreadLocal对象维护一个ThreadLocalMap内部类
- ThreadLocalMap对象又维护一个Entry内部类,并且该类继承弱引用
WeakReference<ThreadLocal<?>>
,用来存放作为key的ThreadLocal对象(可见最下方的Entry构造方法源码),可见最后的源码部分 - 不管当前内存空间是否充足,GC的JVM会回收弱引用的内存
因为ThreadLocal作为弱引用被Entry中的key变脸引用,所以如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收
这个时候Entry中的key已经被回收,但value因为是强引用,所以不会被垃圾收集器回收。这样ThreadLocal的线程如果一直持续运行,value就一直得不到回收,导致发生内存泄露
如果想要避免内存泄漏,可以使用ThreadLocal对象的remove()方法
七、为什么ThreadLocalMap的key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
为什么要这样设计,这样分为两种情况来讨论:
- key使用强引用:只有创建ThreadLocal的线程还在运行,那么ThreadLocalMap的键值就都会内存泄漏,因为ThreadLocalMap的生命周期同创建它的Thread对象
- key使用弱引用:是一种挽救措施,起码弱引用的值可以被及时GC,减轻内存泄漏。另外,即使没有手动删除,作为键的ThreadLocal也会被回收。因为ThreadLocalMap调用set、get、remove时,都会先判断之前该value对应的key是否和当前调用的key相等。如果不相等,说明之前的key已经被回收了,此时value也会被回收。因此key使用弱引用是最优的解决方案
八、父子线程如何共享ThreadLocal数据
- 主线程创建InheritableThreadLocal对象时,会为t.inheritableThreadLocals变量创建ThreadLocalMap,使其初始化。其中t是当前线程,即主线程
- 创建子线程时,在Thread的构造方法,会检查其父线程的inheritableThreadLocals是否为null。从第1步可知不为null,接着将父线程的inheritableThreadLocals变量值复制给这个子线程
- inheritableThreadLocals重写了getMap, createMap,使用的都是Thread.inheritableThreadLocals 变量
如下:
第 1 步:对 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
第 2 步:创建子线程时,判断父线程的 inheritableThreadLocals 是否为空。非空进行复制
// Thread 构造方法中,一定会执行下面逻辑
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
第 3 步:使用对象为第 1 步创建的 inheritableThreadLocals 对象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
}
// 示例:
// 结果:能够输出「父线程-Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父线程-Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();
// 结果:null,不能够输出「子线程-Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
threadLocal2.set("子线程-Java");
});
t2.start();
System.out.println(threadLocal2.get());