前言
一、什么是ThreadLocal
- 变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢,就会用到ThreadLocal。
- Threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。
- ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
二、内部实现
1.set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//在ThreadLocalMap中拿到该线程数据存储 用线程ID作为Map的key
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。
ThreadLocalMap构造方法
/**
* 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) {
//内部成员变量 INITIAL_CAPACITY初始容量是16
table = new Entry[INITIAL_CAPACITY];
//位运算,计算出需要存放的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。
前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。
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;
//这里可以看出同一个Thread中都是共用的一个ThreadLocalMap的成员变量table
//只是取得hash值不同
int i = key.threadLocalHashCode & (len-1);
//如果存在即更新
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//遍历不存在 则创建新的值
tab[i] = new Entry(key, value);
int sz = ++size;
//满足条件 数组扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2.引用结构图及内存泄漏原因
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
如何解决内存泄漏
调用remove()方法,清除数据。
总结
- 对于某一ThreadLocal来讲,他的索引值i(线程id)是确定的,在不同线程之间访问时访问的是不同的Entry[]数组的同一位置即都为Entry[][i],只不过这个不同线程之间的Entry[]是独立的。
- 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个Entry[]数组,然后每个ThreadLocal实例在Entry[]中的索引i是不同的。
- ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:
(1)这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
(2)当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。- 在每个线程中,都维护了一个threadlocals对象,在没有ThreadLocal变量的时候是null的。一旦在ThreadLocal的createMap函数中初始化之后,这个threadlocals就初始化了。以后每次ThreadLocal对象想要访问变量的时候,比如set函数和get函数,都是先通过getMap(Thread t)函数,先将线程的map取出,然后再从这个在线程(Thread)中维护的map中取出数据或者存入对应数据。
- 大白话理解:每个线程都有线程id,ThreadLocal根据线程id找到ThreadLocal的静态内部类,ThreadLocalMap类,这个ThreadLocalMap类同时也是Thread中ThreadLocal成员的一个属性。如果找不到,则构造ThreadLocalMap类,把里面的成员变量table(Entry[]),赋值,初始容量16。然后用ThreadLocal对象按位计算Hash值。把Entry对象(k:ThreadLocal对象,v:值),存进去。所以get的时候,直接用Thread中的ThreadLocal对象计算hash,从ThreadLocal对象的table中,把Entry取出来,再取value值。