深入剖析Java中ThreadLocal原理

1. ThreadLocal 的概念

ThreadLocal 是 Java 中的一个类,用于实现线程本地存储。它允许你创建一个变量,这个变量在每个线程中都有自己独立的副本,不同线程之间的副本互不干扰。换句话说,ThreadLocal 提供了一种线程隔离的机制,使得每个线程都可以拥有自己的变量实例。

1.1 ThreadLocal 的基本用法
ThreadLocal<String> threadLocal = new ThreadLocal<>();

// 在主线程中设置值
threadLocal.set("Main Thread Value");

// 在子线程中设置值
new Thread(() -> {
    threadLocal.set("Child Thread Value");
    Log.d("ThreadLocal", "Child Thread: " + threadLocal.get()); // 输出: Child Thread Value
}).start();

// 在主线程中获取值
Log.d("ThreadLocal", "Main Thread: " + threadLocal.get()); // 输出: Main Thread Value
1.2 ThreadLocal 的工作原理

ThreadLocal 内部维护了一个 ThreadLocalMap,这个 ThreadLocalMap 是每个线程私有的。当你调用 ThreadLocal.set(value) 时,实际上是将 value 存储在当前线程的 ThreadLocalMap 中,键是 ThreadLocal 实例本身。当你调用 ThreadLocal.get() 时,会从当前线程的 ThreadLocalMap 中获取与 ThreadLocal 实例关联的值。


2. ThreadLocal 剖析

概念很简单,一看就会,但让你详细介绍下其原理,那可能就懵比了,问原理,我会概念:)

既然,ThreadLocal 是用于实现线程本地存储,那么它和 Thread 的关系是怎样的呢?

ThreadLocal 的数据之所以与线程绑定,是因为其内部实现依赖于每个线程私有的存储空间。具体来说,ThreadLocal 利用了线程内部的 ThreadLocalMap 来存储和管理每个线程独立的数据副本。

public class Thread implements Runnable {
    // 每个线程私有的 ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals;

    // 其他成员和方法...
}
2.1 每个线程都有自己的 ThreadLocalMap

在Java中,每个 Thread 都有一个名为 threadLocals 的成员变量,类型为 ThreadLocal.ThreadLocalMap,它是线程私有的,其他线程无法访问。ThreadLocalMap 是一个自定义的哈希表,其Entry结构如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

可以看出,Entry 继承自 WeakReference<ThreadLocal<?>>,这意味着对 ThreadLocal 实例的引用是弱引用,而 value 则是强引用,这个就是 ThreadLocal 会造成内存泄漏的关键点,后面详细分析。

弱引用WeakReference)在垃圾回收(GC)中的行为如下:

弱引用对象:如果一个对象只被弱引用所引用,而没有强引用(Strong Reference)指向它,那么在下一次垃圾回收时,这个对象就会被回收掉。
应用场景:弱引用常用于实现缓存等需要在不阻止对象被回收的情况下引用对象的情况。

ThreadLocalMap 中,使用弱引用可以确保当没有其他强引用指向某个 ThreadLocal 实例时,该实例可以被垃圾回收,从而避免因为 ThreadLocal 实例本身无法被回收而导致整个 Entry 一直存在。

2.2 ThreadLocal 存储数据

通过ThreadLocalset方法可以看到 set 值的时候,通过获取当前线程 获取 ThreadLocalMap ,threadLocals,如果获取到了 就拿当前的 ThreadLocal 为键,存储当前值,如果为null,就代表还为初始化,就初始化赋值,然后将 ThreadLocal 实例作为键,将 value 作为值,存储到 threadLocals 中。

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 getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2.3 ThreadLocal 存储数据

get值的时候,同样是获取当前线程 获取 ThreadLocalMapthreadLocals,如果获取到了,就用当前的 ThreadLocal去拿对应的值,为null 不存在,则返回初始值,内部就是 初始化 threadLocals 和存入并返回null值。

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();
}

可以看出,之所以与线程这么强相关,就是每次读取都要通过Thread.currentThread()获取当前线程,然后再去操作数据,而所有的数据都在 Thread 中的 threadLocals进行存储,这下是不是瞬间清楚它们的关系了。

2.4 线程隔离的机制

因为每个线程都有自己的 threadLocals ,并且 ThreadLocal以自身为键,存储数据,每个线程都可以独立获取自己的ThreadLocal` 值,线程之间互不影响,对其他线程数据不可见,确保线程存储私有性。


3. 关于 ThreadLocal 会导致内存泄漏问题

先来看下ThreadLocalMap 的结构

键(Key)ThreadLocal 实例,通过 WeakReference 引用。
值(Value):存储的对应值,强引用。

弱引用的特性​​:如果一个对象只被弱引用指向,而没有强引用指向它,那么在下一次垃圾回收时,这个对象会被回收。

注意是只被!代表其他地方已经不再使用了,已经是垃圾了,才会被回收,而不当GC到来时,弱引用对象就没了,我之前对此有过疑惑,再这里再强调下。

根据内存泄漏定义:长生命周期对象引用了短生命周期对象可知ThreadLocalMapkeyThreadLocal 的弱引用(WeakReference),但 value是强引用,如果 ThreadLocal 本身被 GC 回收了,但 Thread 一直存活(如线程池中的线程),那么 value 就不会被清除,从而造成内存泄漏。

引用链示意图

Thread (线程池中的线程)
└── threadLocals : ThreadLocalMap
    └── Entry[] table
        └── Entry : WeakReference<ThreadLocal> → null (ThreadLocal 已被回收)
            └── value → [object]  (value 强引用未被清理)

这个 value 虽然没有人用了,但由于它还在 ThreadLocalMap 中,所以不会被 GC,造成内存泄漏。
解决的办法也很简单,手动调用下 ThreadLocal.remove()

最后再明确一下概念,每个ThreadLocal 只保存一个值,多次 set 不会叠加数据,而是会替换原有值,真正的内存泄漏风险来自未清理的 value 而非 set 操作本身,由于ThreadLocal 是直接操作当前线程,数据的存储最终是保存在 ThreadThreadLocalMap 中,由 threadLocals 统一管理。

结束了么?并没有,既然要剖析,那就再多了解一点吧;

4. ThreadLocal 中 set 方法源码分析

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;  // 情况1:键已存在,直接更新值
            return;
        }

        if (k == null) {      // 情况2:发现过期Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value); // 情况3:新增Entry
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap 使用开放地址法解决哈希冲突。

从初始索引开始,遍历 ThreadLocalMap 中的槽位,查找是否已经存在相同的 ThreadLocal 键。

主要包括三种处理情况:键存在、键过期、键不存在。
注意ThreadLocal<?> k = e.get();其中 e.get()是获取 ThreadLocal的引用,如果已经被GC回收了,则返回null,如果遇到 keynullEntry,表示这是一个过期的条目(由于弱引用被回收),调用 replaceStaleEntry 方法来替换这个过期条目,注意,replaceStaleEntry 内部仍会调用 cleanSomeSlots 进行尝试清理链表,维持哈希表结构。

最后是尝试清理一些过期的条目,如果清理后仍然达到阈值,则进行扩容操作。

所以可知,即使不显式调用 remove 方法的情况下,ThreadLocalMap 内部的 cleanSomeSlots 方法仍然有可能帮助清理过期的数据,只是有概率会,但这并不保险,最好还是手动调用 remove 方法。


再补充个知识点,ThreadLocalMap 在插入数据时 也会产生哈希冲突, 但于 HashMap 的方式有相似之处,但又有明显不同

ThreadLocalMap 使用开放地址法(线性探测 + 扫描回环)处理哈希冲突,而不是像 HashMap 那样用链表或红黑树。它是一个固定长度的数组,冲突时会向后一个个找空位(或可复用的 stale 位置),必要时回环头部,并会定期清理 stale(已过期)项。

想要详细了解可以慢慢分析 replaceStaleEntry方法。

相同的,就算产生了hash冲突,在 get 时,它从 hash 计算出的索引开始,向后线性探测,逐个比较 entry.get() == 当前ThreadLocal对象,一旦找到匹配,就返回 value。由于插入时也是按此顺序探测,因此一定能找回来。


5. 最后

看完你应该对ThreadLocal有个全新的认识了,虽然概念很容易理解,但我们知其然更要知其所以然。通过现象看本质。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值