介绍
ThreadLocal 中设置的值仅属于当前线程,该值对其他线程而言是隔离的,所以在同一时间并发修改一个属性的值也不会互相影响。
使用
在使用 ThreadLocal 时,可以直接通过set(T value)
、get()
来设置 threadLocal 的值、获取 threadLocal 的值。
set 方法
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) { // 如果map不是空
map.set(this, value); // 设置值
} else {
createMap(t, value); // 创建并设置值
}
}
// 获取线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 对该ThreadLocal设置值
private void set(ThreadLocal<?> key, Object value) {
// ThreadLocalMap内部的table数组
Entry[] tab = table;
int len = tab.length;
// 根据threadLocal的hash和长度进行与运算,找到下标位置
int i = key.threadLocalHashCode & (len-1);
// 曾经该threadLocal有值,设置值并返回
for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
// 获取entry的引用
ThreadLocal<?> k = e.get();
// 引用等于当前threadLocal 则进行设置值
if (k == key) {
e.value = value;
return;
}
// 当前引用为空,把key、value组装成entry放到i位置上,并清楚key为空的entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 组装entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 如果没有元素被清楚,并当前数组大小大于threshold则进行rehash;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
其中 threshold = len * 2 / 3,它是通过setThreshold
方法进行设置的。而每次rehash
的时候都会调用resize
方法,它会读取 oldTable.length,把newLen
设置为oldLen
的两倍。
这里有一个注意点int i = key.threadLocalHashCode & (len-1);
下标是通过 hash 来确定的,会出现 hash 冲突,这里采用的是开放地址法来解决 hash 冲突,在下面的代码中有判断k==key
,如果不相等则nextIndex(i, len)
获取下一个下标来判断。
上述就是整个set
的过程,下面来看一下get
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this为当前threadLocal,获取对应的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 返回当前entry的值即可。
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始值并返回,初始值是null
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
// 查找下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 找到对应entry进行返回
return e;
else
// 开始遍历entry数组,如果能找到key的entry就返回否则返回null
return getEntryAfterMiss(key, i, e);
}
get
方法要比set
简单很多,只是根据 key 找对应 entry,把 entry 的值返回即可。
结构
通过上述源码,可以总结出 threadLocal 的数据结构如下:

问题
根据上面的介绍,可以看出一些潜在的问题;例如在使用 threadLocal 时堆栈信息如下:

真的会内存泄漏?
当使用完 threadLocal,threadLocal 的对象引用就不存在了,而 key 对 threadLocal 是弱引用,gc 后这段引用也不存在了。此时无法通过map.getEntry(this)
找到对应的 entry,而 entry 还一直存在 Entry[]中,就有可能
导致了内存溢出。这里我写了是有可能导致内存溢出,例如在set
方法中有这样一行代码
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
该方法的具体代码如下:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
当有新的 threadlocal 进行设置值时都会进行清除一下e.get() == null
引用为空的 Entry,而进入到这里的条件是(n >>>= 1) != 0
,当长度为 16(10000)会触发 5 次,挨着当前 threadlocal 的 Entry 的连续 5 个都没有引用为 null 的话,就不会继续往下移除了。
所以如果频繁的调用set
方法,它也会帮助清除一些之前 key 已经被 gc 掉的 entry 对象,但无论如何如果没有 gc 和调用set
方法的话,这些 entry 对象会一直在内存中占用。
所以每次在使用完 threadlocal 时要调用一下remove
方法,它会自动把 entry 移除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
除此之外在 threadlocal 时,尽量把它设置为 pricate static 变量,这样因为 threadLocal 的强引用一直存在,不会被垃圾回收掉这样就能保证任何时间都可以找到 Entry,并对其进行remove
。
Entry 的 key 设置为强引用可以么?
当 ThreadLocal 的引用在用户栈中已经移除了,并且没有调用remove
方法;但是 entry 还有一个强引用指向 threadLocal 对象,e.get()
永远都不会是空,此时 entry 对象就永远无法被回收掉了。
这样弱引用比强引用就多一层保障弱引用的 ThreadLocal 会被回收,对应 value 在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。
示例:
public static void main(String[] args) {
for (int i = 0 ; i < 100 ; i ++){
ThreadLocal temp = new ThreadLocal();
temp.set(i);
temp = null;
}
// System.gc();
ThreadLocal m = new ThreadLocal();
m.set("value");
}
感兴趣的话,可以用上述示例跟着源码跑一遍源码的流程,当开启System.gc();
时可以走到清理回收阶段。
子线程可以使用父线程的 threadLocal 中的值么?
不可以,如果想使用的话可以采用InheritableThreadLocal
,它会在初始化子线程时进行设置子线程的 threadlocal,也仅仅在初始化时有关联,后续子线程和父线程互相更改 threadlocal 都不会有任何影响。示例:
private static InheritableThreadLocal threadLocal = new InheritableThreadLocal();
@SneakyThrows
public static void main(String[] args) {
threadLocal.set("1");
Thread thread = new Thread(
() -> {
System.out.println("子线程获取threadLocal的值为:" + threadLocal.get());
threadLocal.set("2");
}
);
thread.start();
Thread.sleep(200);
System.out.println("父线程获取threadLocal的值为:" + threadLocal.get());
}
1、父线程先设置 threadLocal 的值为 1;
2、开启一个子线程,获取 threadLocal 的值,得到结果为 1;
3、子线程设置 threadLocal 为 2,并且 get 一下,得到的结果为 2;
4、睡眠 200ms 确保子线程命令都执行完成;
5、父线程获取 threadLocal 的值,得到的结果为 1。
构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!
文明发言,以
交流技术
、职位内推
、行业探讨
为主
广告人士勿入,切勿轻信私聊,防止被骗

点下方的“❤”支持我们,非常感谢!
原文始发于微信公众号(Java面试题精选):面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/316804.html