ThreadLocal 底层实际原理以及应用场景

很多朋友在面试时常常都会被面试官问到 ThreadLocal 的底层原理以及使用场景,或者是自己有使用该类,但是对其还是存在部分的疑惑,那么不妨看看我这篇文章。

1 Thread, ThreadLocal 类结构解析

Thread 类当中存在一个唯一绑定的 ThreadLocalMap 对象,在这个 map 当中就是我们实际存放数据的。

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal 是用于 ThreadLocalMap 的弱引用 key, 对于弱引用,我们都知道只能活到下一次 gc, 但是很多朋友在这里没有想明白,如果刚存入值,但是就发生了gc, 那么这个弱引用的 key 会被回收吗?答案是不会的,因为 map 虽然对 key 是弱引用,但是在外面, 还有线程对 ThreadLocal 对象的强引用啊, 那么这里可以得出的结论就是 ThreadLocal 是和线程当中对其强引用的占有多久,只有线程对其没有了强引用,那么其才会被 gc 。而 map 对象是和线程同生命周期的。讲解到这里大家都应该能够听明白吧。map 当中也是通过 key - value 实现的。并且 key 为弱引用实现。

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

2 ThreadLocal 源码解析

1 通过以下方式对 ThreadLocal 进行创建以及初始化定义,这里设置初始化定义的数据是很有必要的。虽然不设置也是可以运行的,但是后面操作获取数据可能会导致空指针异常,那么通过设置初始化的就可以避免。

ThreadLocal<String> local = ThreadLocal.withInitial(() -> "");

2 数据存入, 通过 ThreadLocal 对象就可以直接对数据进行存放操作。

local.set("hello world");

在方法当中,其第一步是获取到当前的线程对象,调用getMap(t) 方法, 获取和 Thread 进行唯一绑定的 ThreadLocalMap 对象。这里其 map 的生命周期是和线程是一样的,很多同学对这里的认识是可能有错误的,因为唯一对 map 有引用的只有 Thread 对象。

public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程对象
    ThreadLocalMap map = getMap(t); // 通过线程对象获取到 map
    if (map != null) // 判断是否有 map
        map.set(this, value); // 存在 map 放入值
    else
        createMap(t, value); // 不存在 先创建 后放入
}

这里就是通过线程获取到其绑定的唯一 map 方法。

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

没有 map 对象,进行创建操作。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

通过创建数组方式创建桶数组,通过 hash 与运算得到其实际的插入数据的桶下标位置。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY]; // 创建桶数组
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 计算第一个节点hash位置
    table[i] = new Entry(firstKey, firstValue); // 放入
    size = 1; // 大小设置
    setThreshold(INITIAL_CAPACITY); // 扩容阈值
}

和HashMap 类似,通过 hash 计算桶位置,这里通过一个循环,找到不为 null, 也就是没有存放数据的位置,就可以退出循环,插入 entry 数据。

这个循环我详细的解释一下,通过 i 表示其需要查找的下标,如果存在 entry 不为空,进入查看,判断是否为 key 重复,key 重复做替换操作,但是下面还有一个判断条件, 也就是 k == null。 这里和内存泄露相关联了,因为 jdk 防止因为使用错误没有做删除,会导致 key 被回收,但是值还存在的情况所做的优化,发现了这种 key 已经被 gc 了的 entry, 那么可以帮助删除原来的 value, 并且可以占据该位置。而循环变换的条件是获取下一个节点,这里也就是解决 hash 冲突的方式了,和 HashMap 不一样,其没有使用链地址法,而是使用线性探测法。对于解决哈希冲突可以看看这篇博文 解决哈希冲突(四种方法)

而最后的代码也就是对扩容进行操作。其扩容策略是所有的空间都使用完毕了,超过其阈值了就会进行扩容。

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) { // 判断是否为key 替换
            e.value = value;
            return;
        }
        if (k == null) { // 没有 key 替换 内存泄露的问题
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value); // 直接遇到为空的桶位置 插入
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 判断是否扩容
        rehash();
}

以上就是对 ThreadLocal 当中获取数据的完整流程

3 数据获取

直接通过 get 方法就可以去获取数据值。

local.get();

和 get 一样,都是先获取当前线程,通过线程获取到 map。 再通过 ThreadLocal 作为 key 去获取到 entry 对象。最后做结果的返回。但是没有,就会返回设置的初始化的值,所有在我们使用 ThreadLocal 的时候应该先将其设置默认值,防止空指针。

public T get() {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 获取 map
    if (map != null) { // 存在 map 才进行操作
        ThreadLocalMap.Entry e = map.getEntry(this); // 通过 ThreadLocal 获取数据值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value; // 结果返回
            return result;
        }
    }
    return setInitialValue(); // 没有 map 返回默认值
}

同样具体的查找都是先计算桶位置,第一次直接拿到就返回,没有能够拿到那么就线性探测。

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
        return getEntryAfterMiss(key, i, e); // 通过线性探测查找
}

这个是线性探测的获取结果的方法,通过 entry 的 key 进行查找,key 如果找到了具体的值,那么说明确实找到了结果,返回,如果出现了 key 为空的情况,这里又出现了内存泄露的问题, 这里 jdk 也是做了优化,会将这种发生了内存泄露的 entry 进行清除。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get(); // 获取 entry 的 key
        if (k == key) // 找到结果 返回
            return e;
        if (k == null) // key 为空 entry 存在 内存泄露问题
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len); // 线性探测
        e = tab[i];
    }
    return null;
}

4 手动清除 value

虽然 jdk 为我们做了防止内存泄露的优化,但是我们还是要手动对 value 进行清除,因为可能出现极端情况,jdk 的自己解决内存泄露没有发生。那么整个程序就可能出现危机。

local.remove();

相信有了以上的基础,这里的代码应该很清除了。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread()); // 获取线程
    if (m != null)
        m.remove(this); // 清除
}

3 误区解读

1 很多人就对为什么会发生内存泄露,以及 key ,value, map 的生命周期没有一个明确的认识。

下面是我画的一个引用关系图,其中实线为强引用,虚线为弱引用。

 ThreadLocal 作为弱引用的 key ,但是还收到了 Thread 的强引用,也就是说,当跳出了相应的调用栈,ThreadLocal 的生命周期就结束了。而 map 是一直被 Thread 强引用着,其生命周期就和线程生命周期一样。通过以上可以得出的结果,对于一般创建了很快就会死亡的线程, key 和 map 和 thread 线程生命周期基本一样,而在线程池当中一直不会死亡的线程,map 和 thread 一直会存活,而 ThreadLocal 是出了相应的调用栈,没有了线程对其的强引用, 其生命就结束了。所有对于线程池当中使用的 ThreadLocal 一定要手动清除 value 就是这样一个原因。

2 ThreadLocal 其的到底是一个什么作用呢?

ThreadLocal 很多人都会和线程安全等等联系起来,确实,这个我不否则,其确实是线程安全的,但是这并不是其正在的作用,其主要的作用是实现数据的私有化,hreadLocal为每一个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果无非是耗费了内存,也大大减少了线程同步所带来的性能消耗。并且实现了线程的隔离。

3 ThreadLocal 能够抗住高并发?

答案是不能的,ThreadLocal 设计的目的只是为了实现数据的线程私有,线程隔离作用,根本都不会产生线程安全问题,没有数据的共享,其如何来提高并发呢?如果要考虑高并发,可以多考虑硬件,cpu, 优化共享数据的同步访问,加入缓冲等等。所以别把 ThreadLocal 和并发考虑在一起了。

4 ThreadLocal 没有并发问题,这么好,是不是应该多使用?

答案也是不能的,合适即可,使用在一些需要的特定场景就够了,而不能一贯的使用,在数据量很小的情况下可能感觉其访问速度还很快。但是在大数据量的情况下,其性能就会很差了。首先其使用的解决哈希冲突方式为线性探测法,时间复杂度那可是 O(n) 呢,并且其扩容操作,是必须要将所有的容量都使用完毕才会进行扩容。

4 使用场景

1 一个用户一个线程操作,存放用户信息:用户登录令牌解密后的信息传递、用户权限信息、从用户系统中获取到的用户名。

2 做数据库的session 连接,将数据库连接和线程进行绑定。

3 对数据做隐式传递,一些情况下,方法被限定好了,我们要做参数的传递等等操作时,就可以放入 ThreadLocal 来传递数据。

5 总结

总体来说, ThreadLocal 是通过 Thread 和 ThreadLocalMap 来实现线程隔离的类,使用过程可能出现内存泄露问题,需要我们进行手动清除。了解了其底层查找方式后,也就明白了为什么不能大量使用,了解了其几个的生命周期之后,也就知道了为什么尽量不要在线程池当中使用 ThreadLocal。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值