目录
1、什么是ThreadLocal?有什么用?
ThreadLocal 是 Java 并发模型中的一个关键工具类,用于实现线程隔离(Thread Confinement) ,即:为每个线程提供独立的变量副本,避免多线程之间的共享与竞争。
简单理解:ThreadLocal可以实现线程之间的变量互相隔离,也就可以理解为线程的私有数据。
例如:
我们常用的将用户ID存储到ThreadLocal中就可以实现当前线程中可以一直使用。
多数据源时将当前线程使用的数据源存在ThreadLocal中后续使用。
2、ThreadLocal方法、概念详解
void set(T value)方法详解
当我们调用set方法时,可以看到首先获取到当前线程,然后调用getMap方法,这个getMap方法返回了一个ThreadLocalMap方法,然后如果map不为空,将当前ThreadLocal对象和值转为Entry对象放入到这个map中。
tab[i] = new Entry(key, value);
由此我们可以看出当我们调用set方法时,是将当前ThreadLocal对象作为key,value作为值构建了一个Entry对象,然后存入到ThreadLocalMap对象中。
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 这个是上面的map.set方法
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;
}
}
// 将当前示例对象ThreadLocal、value作为Entry对象存入
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap是Thread类中的的一个成员变量
void remove()方法详解
remove方法就很简单了,就是获取当前线程的ThreadLocalMap对象,然后清理。
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
T get()方法详解
获取ThreadLocalMap,将当前ThreadLocal对象作为Key存储,如果值不为空就返回,为空则返回null
java
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();
}
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;
}
Thread、ThreadLocal、ThreadLocalMap的关系
看了上面可能会疑惑Thread、ThreadLocal、ThreadLocalMap都有什么关系呢?
ThreadLocalMap是Thread的一个成员变量,ThreadLocal是作为key存储在ThreadLocalMap中的
Thread
└── ThreadLocalMap(每个线程独有)
├── Entry(ThreadLocal1 → value1)
├── Entry(ThreadLocal2 → value2)
├── Entry(ThreadLocal3 → value3)
Entry是什么
结论:entry对象是存储在ThreadLocalMap中的基本对象。ThreadLocalMap中寸的就是一个个的entry对象。从ThreadLocal的set方法中可以看出:
就是将当前ThreadLocal对象作为key,要存的值作为value构建Entry对象放入ThreadLocalMap中
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 这个是上面的map.set方法
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;
}
}
// 将当前示例对象ThreadLocal、value作为Entry对象存入
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
根据代码可以看出Entry对象的key是弱引用,value是正常的也就是强引用,这也是后面会发生内存泄漏的关键。
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
3、ThreadLocal的常见问题:
3.1什么是内存泄漏
标准回答:程序中某些对象已经"不再被使用",但仍然被引用,导致 GC 无法回收,从而长期占用内存。
大白话:对象创建了,但是使用完没销毁,导致一直存放在内存中,白白占用空间,还回收不了。
3.2为什么必须调用 remove?不remove为啥会内存泄漏?
首先我们要明白ThreadLocal是如何存放数据的。
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
存入的key是弱引用,value是强引用,如果在正常请求中我们请求结束也就没有引用了,就可以被正常的gc回收掉。
但是如果在线程池中,线程是复用的,如果没有调用remove方法,就会导致这个对象不再被使用。但是一直存在,而且还是强引用,被回收不掉,就发生了内存泄漏。
GC Roots
↓
Thread(线程池线程不会销毁)
↓
ThreadLocalMap
↓
Entry
↓
value
例如:
我在使用线程池的时候,每次都new一个ThreadLocal对象,然后存当前用户的信息
当我第一次调用时
ThreadLocal.set(new ThreadLocal(),userInfo1)
第二次调用时
ThreadLocal.set(new ThreadLocal(),userInfo2)
频繁下来,userInfo就一直存在内存中没有清楚,而且还可能导致数据错乱
3.3为什么 ThreadLocalMap 的 key 要设计为弱引用?如果是强引用会有什么问题?
ThreadLocalMap 的 key 设计为弱引用,是为了避免 ThreadLocal 对象无法被回收,从而导致更严重的内存泄漏
弱引用能保证GC可以回收ThreadLocal对象
- 在线程池中,线程对象不会被销毁
- 如果 key 是强引用 → ThreadLocal 永远被 Thread 持有 → value 也无法回收 → 内存泄漏严重
3.4异步场景为什么会丢上下文?
异步场景会丢失上下文,是因为 ThreadLocal 的数据存储在"当前线程"中,而异步任务通常运行在"另一个线程"上
Thread A
└── ThreadLocalMap
└── user = A
Thread B
└── ThreadLocalMap
└── user = null
线程之间没有共享
例如:
java
ThreadLocal<String> tl = new ThreadLocal<>();
// 主线程
tl.set("userA");
executor.submit(() -> {
System.out.println(tl.get()); // 拿不到
});
主线程(Thread A)
↓ set(userA)
线程池线程(Thread B)
↓ 执行任务
↓ get() → 取自己的 ThreadLocalMap → 没有数据
4、多线程下如何实现上下文的传递
TransmittableThreadLocal
TTL 可以在"线程切换(尤其线程池复用)"时,把 ThreadLocal 的值从提交线程传递到执行线程
- 捕获上下文(提交任务时)
主线程:
ThreadLocalMap → 拷贝一份
- 设置上下文(执行任务时)
执行线程:
把拷贝的数据 set 到当前线程
- 恢复现场(执行完成)
恢复线程原有的 ThreadLocal 数据
👉 防止污染线程池线程
