前言
在日常开发中,有时候用ThreadLocal 来传递上下文或者某些变量是非常方便的,因为传递的对象是绑定在线程上的。那么ThreadLocal是如何实现的呢? 以及使用过程中有哪些注意事项? 我们跟着源码来一起看一下
原理
上面说道传递的对象是绑定在线程上的,这是如何实现的呢?
在Thread 类中有一个成员变量 threadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
//ThreadLocalMap 定义
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" 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 ,这个threadLocalMap 是一个 (k,v)的存储形式,其中 k 就是当前线程对象, v 就是我们存储的对象。
下面我们看下 ThreadLocal的 set方法来进行验证
set 方法
public void set(T value) {
// 1. 获取当前活动线程
Thread t = Thread.currentThread();
//2. 根据当前线程获取
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
根据当前线程 t 获取对应的 threadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//threadLocalMap 中的设值方法
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();
}
上述set 方法中,需要留一下这段代码 ,后面我们会再提到这一块
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
get 方法
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;
}
}
//没有map的话 ,则进行初始化
return setInitialValue();
}
// 设置初始值 null
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
看完上述两个方法,大体上这个对象也就清晰了
调用 ThreadLocal.set(对象A) 方法时,实际上就是在当前线程对象上的属性 threadLocalMap 上做一个set(当前threadLocal指针, 对象A)的操作
内存泄漏的情况和原因
泄漏案例
看一段代码
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<Room> local = new ThreadLocal<>();
local.set(new Room());
local = null;
System.out.println(local);
}
}
相信很多人都知道,这种情况是有内存泄漏的,也就是执行GC后,threadLocalMap里的对象没有被回收。
在 local = null 这一行打断点,打开 java Visualvm观察到这一行前后(进行GC)对象数量没有变化。
local = null 还未执行的对象数量
local = null 已经执行过,并且进行了GC
未泄漏的案例
下面这一段不会发生内存泄漏的代码
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<Room> local = new ThreadLocal<>();
local.set(new Room());
local.remove();
local = null;
System.out.println(local);
}
}
local = null 还未执行的对象数量 其中 entry 18个 ,threadLocalMap 4个
local = null 已经执行过,并且进行了GC
很明显,通过上述两段代码,我们验证了 threadLocal 内存泄漏的情况,因此每次使用完后需要手动执行 remove 方法。
为什么会泄漏
我们跟着代码来讲一下为什么第一段会内存泄漏
ThreadLocal<Room> local = new ThreadLocal<>();
local.set(new Room());
在这里的时候,实例化了一个 new ThreadLocal 对象 并指向了 local ,同时还实例化了一个 Room对象;
local = null;
当我们执行 local = null 时 ,断开了 local 与 new ThreadLocal 对象之间的联系。
按道理来说,new ThreadLocal 这个对象应该要被GC回收。
但是这个对象中的 threadLocalMap 中有一个entry。这个entry的key 是当前threadLocal的指针 ,value 是 room 对象。
也就是说只要当前线程还存活,这个entry就是有引用的,因此这个 new ThreadLocal 也是不会被回收,也就发生了内存泄漏的情况。
弱引用的好处
还记得我们上面说的 ,其中 k ==null 那一部分的代码吗
//threadLocalMap 中的设值方法
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;
}
// k 为 null 时,清除掉对应的value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
也就是同一个线程,第二次做set操作时是清除掉之前 k = null (防止对应的threadlocal指针被置空)对应的value值 。
为什么要用弱引用呢? 准确地说是 entry的 key 为弱引用 。
public class ThreadLocalTest {
public static void main(String[] args) {
test2();
System.out.println(111);
}
public static void test2() {
ThreadLocal<Room> local = new ThreadLocal<>();
local.set(new Room());
System.out.println(local);
}
}
在test2 方法执行完,但 main 方法未执行完时。
因此 test2执行完,栈帧都销毁了, local的强引用也就没了,此时有一个 threadLocal 对象里还有个 threadLocalMap。
此时我们发现,threadLocal对象只会被 Entry引用。如果entry 的key 是个强引用,那么threadLocal就不可回收了;但是如果是个弱引用,那么这个key就会被回收,entry就会变成 (null , value) 。
实际上我们翻看remove方法就会发现,remove的时候是同时把key value都置为 null 了。