一、何为ThreadLocal
threadLocal是线程存储信息的一种手段,方便我们在多线程环境下,分层(比如MVC)开发中存储信息的一种手段,比如存放登录用户信息,多租户环境下的来源信息,分库分表/读写分离的信息。
二、简单使用
首先,生成一个子线程,往ThreadLocal中存放一个thread A
的信息,主线程无法获取到,再生成一个thread B
同样无法获取到thread A
,说明了ThreadLocal
具有线程隔离性,但是线程B获取到了thread B
说明同线程当中,是可以获取到的。
// 定义 ThreadLocal
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
// 先放一个信息到ThreadLocal, 然后主线程再取,证明主线程不会访问到
new Thread(() -> {
stringThreadLocal.set("thread A");
},"thread A").start();
Thread.sleep(2000);
System.out.printf("线程:%s,值:%s%n",Thread.currentThread().getName(),stringThreadLocal.get());
new Thread(() -> {
System.out.printf("线程:%s,值:%s%n",Thread.currentThread().getName(),stringThreadLocal.get());
stringThreadLocal.set("thread B");
System.out.printf("线程:%s,值:%s%n",Thread.currentThread().getName(),stringThreadLocal.get());
},"thread B").start();
输出结果
线程:main,值:null
线程:thread B,值:null
线程:thread B,值:thread B
进程已结束,退出代码为 0
三、源码解读
3.1 构造方法
ThreadLocal
类没有继承其他类或者实现任何接口,所以只有一个无参构造方法(其实也提供了一个附带初始化的静态初始化方法withInitial
,内部需要自己实现初始化动作)
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
3.2 调用Set方法
下面是set
方法源代码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
首先使用Thread.currentThread()
来获取当前线程对象,并通过getMap
来获取当前线程的存储结构,通过查看getMap
方法代码 ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
可以看到,这个map
其实就是Thread
类的threadLocals
属性。
然后我们来看一下Thread
类的一部分代码
发现这个存储结构定义在ThreadLocal
类当中,并且初始值为null
,自此大致清楚数据存储在线程当中,存储结构定义在ThreadLocal
类当中.
3.2.1 createMap
当存储对象map
为空时,我们需要对他进行初始化,并赋值给Thread.threadLocals
属性
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
下面看一下这个ThreadLocalMap
是如何进行初始化的
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);
}
可以看到线程存储的信息是放在一个属性名为table
的Entity数组
当中,并且通过ThreadLocal
和INITIAL_CAPACITY
(初始容量)进行按位与运算,得出该ThreadLocal变量
存储的内容在线程对象内部属性threadLocals
数组存储的下标。
3.2.2 set
这块不做深入解读了,大致内容就是遍历整个存储的map,如果内容已经存在就不进行处理了,如果不存在获取到下标,但是key==null,则进行特殊处理,如果没有存储过就加入并进行rehash和扩容。
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);
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();
}
四、内存泄漏
由于对象存储在线程当中,所以当线程使用完毕进行销毁时,存储的内容会随着线程进行销毁。
但是当我们使用线程池时,线程并不会被回收,这就可能会造成线程内存储的内容会一直存在。
回顾一下ThreadLocalMap
当中的Entity数组
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
超出作用域或者被重新赋值,导致其内存地址变更(hashCode改变)了,那么在此之前通过该对象在线程当中存储的内容进行游离状态,无法被访问,也不会被回收,自此造成内存泄漏,严重可导致内存溢出,抛出OOM异常,导致程序崩溃。
如何避免内存泄漏:使用完立刻调用remove
方法,删除所使用的引用。