面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?

面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?

介绍

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 有自动清除机制还存在内存泄漏?
image.png

问题

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

面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?
image.png

真的会内存泄漏?

当使用完 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。

来源:juejin.cn/post/7395024434681937971
后端专属技术群

构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!

文明发言,以交流技术职位内推行业探讨为主

广告人士勿入,切勿轻信私聊,防止被骗

面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?
加我好友,拉你进群

点下方的“❤支持我们,非常感谢!

原文始发于微信公众号(Java面试题精选):面试官:为什么 ThreadLocal 有自动清除机制还存在内存泄漏?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/316804.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!