-
背景
-
ThreadLocal应用场景
-
代码演示
-
自我设计
-
jdk中的设计
-
内存泄漏问题探讨
-
再谈`ThreadLocalMap`弱引用问题
-
总结
-
后续
背景
一直以项目中就经常使用ThreadLocal
,但是对他的原理总是迷迷糊糊,每次想要了解,就打开网上看看别人的博客,发现写的都大同小异,然后又不太清楚,导致自己对ThreadLocal
原理总是一知半解,今天总算有空来自己研究研究ThreadLocal
,今天就带领大家来探讨久违的真相
ThreadLocal应用场景
这里先介绍下ThreadLocal
的应用场景。直接看jdkThreadLocal
源码上的注释(ps:我jdk版本11)可以看到作者是出自大师 Josh Bloch 和 Doug Lea 之手,这段话的说的大概就是:
ThreadLocal
提供线程局部变量。这些变量不同于它们的普通对应变量,访问某个变量(通过其 get 或 set 方法)都是每个线程自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的 private static 字段。这段话看的迷迷糊糊,大致意思就是ThreadLocal
中的变量是每个线程独享的,在这个线程中任何地方都能获取到。应用场景大致有:
-
获取用户信息,我们在web中不同的层级中获取用户信息,可以将用户信息保存在一个 ThreadLocal
中,然后在任何地方都能获取到 -
zull网关中的一些请求过滤链中的一下上下文直接的传递,也是通过 ThreadLocal
-
有时候需要做一些aop拦截方法获取方法参数,也可以使用 ThreadLocal
。
总之ThreadLocal
应用场景非常之多
代码演示
这里我们使用一个简单的代码来验证ThreadLocal
是线程隔离的
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
Integer integer = threadLocal.get();
System.out.println(Thread.currentThread().getName() + ": " + integer);
threadLocal.set(integer + 1);
});
}
executorService.shutdown();
}
可以看到每个线程获取到的变量都是独立的,这里只是为了证明
ThreadLocal
线程间变量的独立性,实际ThreadLocal
不会是这种应用场景,也不会这么使用
自我设计
假设要我们实现这种线程变量隔离的需求,我们需要怎么实现呢?一般也很简单。使用一个Map,key 为Thread,value为 值
public class MyThreadLocal {
private static final Map<Thread, Object> map = new HashMap<>();
public void get() {
map.get(Thread.currentThread());
}
}
这样设计的问题也很明显,不管线程有没有销毁,该线程存储的键值对都不会被回收,因为都在一个map中,除非这个map没有被人引用了。
jdk中的设计
目前来说jdk中的设计相对巧妙中,他是让每个线程拥有自己的ThreadLocalMap
大致是如下方式:为什么会是这样的呢?我们直接通过源码来分析: 直接看
ThreadLocal
的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;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到就是获取到当前线程t
然后通过t
去获取ThreadLocalMap
,而getMap,可以看到就是调用Thread的属性所以通过源码我们证明了上面结构的正确性,然后我们来分析这种设计的优点是什么,就是每个线程自己销毁后
ThreadLocalMap
,不像我们设计的即使线程销毁了,他所拥有的变量还是,这样就避免了内存泄漏。所以这里我们得出了一个非常重要的结论:
==只要当前线程销毁,他所拥有的ThreadLocalMap
一定会被回收,所以不存在内存泄漏问题==
内存泄漏问题探讨
通过上面的结论我们得出了一个很重要的结论:==只要当前线程销毁,他所拥有的ThreadLocalMap
一定会被回收,所以不存在内存泄漏问题== ==所以平时我们线程使用完销毁是不会有有内存泄漏问题的==
那么网上一直流传的内存泄漏问题是否真的存在呢?其实是存在的。大家想想我们平时在做项目的时候自己创建线程的场景多吗?很明显基本没有,因为线程的创建销毁代价太过昂贵,我们一般都是使用线程池,那么问题来了,线程池的线程一般会循环利用,不会销毁。所以网上说的==内存泄漏是发生在使用线程池的场景==
再谈ThreadLocalMap
弱引用问题
我们首先来看看ThreadLocal
的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);
}
}
短短几行,其实也很简单,就是获取到线程的ThreadLocalMap
然后set一个属性,然后我们看看ThreadLocalMap
set方法
-
ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
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();
}
我们看新加值的关键代码
tab[i] = new Entry(key, value);
可以看到Entry
的key 就是我们的ThreadLocal
,在看Entry
的定义
static class Entry extends WeakReference<ThreadLocal<?>> {}
这里就看到了我们说的弱引用,即Entry
的key
是对ThreadLocal
的弱引用,那么现在的引用状态呢,这里借鉴网上的一张图:可以很清晰的看到
Entry
对ThreadLocal
的弱引用。
==注意这里弱引用解决的问题:首先虽然Key
是对ThreadLocal
,但是key
并不会被回收,因为线程Thread对他是强引用,这里解决的问题是为了防止ThreadLocal
自身的内存泄漏,因为如果这里key对ThreadLocal
是强引用,那么在我们将栈中的ThreadLocalref
变量不指向ThreadLocal
,ThreadLocal
也无法被回收,因为还有key对他是一个强引用,如果这是弱引用,ThreadLocal
就可以顺利被回收==
弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
这里证明比较复杂,但我还是引用网上的一段代码去证明 首先证明key 被回收的场景
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object ThreadLocalMap = field.get(t);
Class<?> tlmClass = ThreadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
弱引用key:java.lang.ThreadLocal@433619b6,值:abc
弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
--gc后--
弱引用key:null,值:def
代码来源链接
==注意这里的ThreadLocal
没有被任何变量引用== 但是我们实际的应用场景都是将ThreadLocal
定义为 static final
,那么我们将上面代码作一些改动
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
threadLocal.set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object ThreadLocalMap = field.get(t);
Class<?> tlmClass = ThreadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
可以看到我们验证的结论是正确的,也是==网上所说的key被回收为null的发生内存泄漏其实是错误的==
总结
-
ThreadLocal
只有在线程池下面才会发生内存泄漏 -
ThreadLocalMap
中的key
为对ThreadLocal
的弱引用,只是为了解决ThreadLocal
回收问题 -
ThreadLocal
所谓的内存泄漏问题,不是说key
为null导致,只是说我们在使用ThreadLocal
因为线程没有被回收,而我们对该变量不想再使用了,而一直存在ThreadLocalMap
没有被回收,就算发生了内存泄漏。所以在使用ThreadLocal
注意使用完调用remove
方法
后续
ThreadLocal
这里主要是对网上讨论的内存泄漏问题做了一个必要的辟谣。还有一些核心源码也没有分析,限于篇幅,感兴趣自己可以自己去探讨,比如ThreadLocal
中的黄金分割数等等 这里也推荐一篇ThreadLocal
分析比较全的博客地址
原文始发于微信公众号(小奏技术):ThreadLocal真的存在内存泄漏吗?来这里探讨真相吧
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30417.html