ThreadLocal

1. 简介

ThreadLocal是Thread的局部变量,用于编写多线程程序,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。

能干什么:

  1. 存储数据 & 线程间数据隔离
  2. 在进行对象间跨层传递的时候,使用ThreadLocal可以避免多次传递。比如将用户信息set到ThreadLocal中,当前线程在任何地方都可以通过ThreadLocal对象直接get用户信息,不用为了获取用户信息而把用户信息传来传去

2. 初探

既然说 ThreadLocal 是线程的局部变量,那么一起验证下以下两个问题:

  1. 主线程中set的Hello World ~,在thread-1线程中能否get到
  2. 在thread-1线程中设置的值能否在主线程中get到 & thread-1线程set的值会不会影响主线程中set的结果
@Slf4j public class ThreadLocalTest { final static ThreadLocal<String> tl = new ThreadLocal<>(); @Test @SneakyThrows public void test() { tl.set("Hello World ~"); log.info("{}:value = {}", Thread.currentThread().getName(), tl.get()); new Thread(() -> { log.info("{}:value = {}", Thread.currentThread().getName(), tl.get()); tl.set("Hello Thank You ~"); log.info("{}:value = {}", Thread.currentThread().getName(), tl.get()); }, "thread-1").start(); TimeUnit.MILLISECONDS.sleep(500); log.info("{}:value = {}", Thread.currentThread().getName(), tl.get()); } }

运行打印结果如下:

23:28:20.801 [main] INFO com.ldx.test.threadlocal.ThreadLocalTest - main:value = Hello World ~ 23:28:20.806 [thread-1] INFO com.ldx.test.threadlocal.ThreadLocalTest - thread-1:value = null 23:28:20.806 [thread-1] INFO com.ldx.test.threadlocal.ThreadLocalTest - thread-1:value = Hello Thank You ~ 23:28:21.309 [main] INFO com.ldx.test.threadlocal.ThreadLocalTest - main:value = Hello World ~

小结

通过日志结果可以判断出:主线程与thread-1线程set 和 get 值都互不影响

3. 源码

在看源码之前我们先大概了解一下Thread,ThreadLocal,ThreadLocalMap,Entry都是什么关系?

3.1 ThreadLocal

ThreadLocal类其实很简单,核心方法就set,get,remove

withInitial:这个方法采用Lambda方式传入实现了 Supplier 函数接口的参数。

3.1.1 set()

哈? 超简单有没有。

public void set(T value) { // 获取当前线程(也就是当前是在哪个线程方法内执行当前set方法,获取的就是哪个线程) Thread t = Thread.currentThread(); // 获取一个Map 后面讲此Map ThreadLocalMap map = getMap(t); if (map != null) // this:指的是当前的ThreadLocal实例对象 map.set(this, value); else createMap(t, value); }
  1. 获取当前线程
  2. 获取Map
  3. map不为空就set,为空就create

首先看下getMap方法:

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

昂~ 就是把线程对象threadLocals属性返回去了

等等 🤔,为什么是线程对象的threadLocals属性呢?

看到这里应该就有点明白了,原来ThreadLocal set值的时候操作的是Thread对象中的threadLocals,就说为什么线程间数据隔离呢,原来值都存储在了线程对象自己的成员变量中。

我们继续看下createMap(t, value)具体干了些什么

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

创建了一个ThreadLocalMap对象赋值给了线程的threadLocals属性

小结:

ThreadLocal::set 方法逻辑还是比较清晰的

  1. 获取当前线程
  2. 获取当前线程的threadLocals属性值作为操作的map
  3. map不为空就set,为空就create一个ThreadLocalMap对象赋值给threadLocals

这里先不纠结ThreadLocalMap是个什么东东,就当它是个map

3.1.2 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; } } // 如果map为null 就初始化一个value(其实就是null) return setInitialValue(); }
  1. 获取当前线程 & 返回当前线程的threadLocals作为map
  2. map不为空就根据this(当前的ThreadLocal实例对象)获取到Map的Entry对象
  3. 如果Entry不为null就返回

这么一看get方法也没啥

3.1.3 remove()

public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }

这?就没啥好说的了

3.2 ThreadLocalMap

在看ThreadLocal方法时 反复提到了ThreadLocalMap对象 并且 对象方法的操作其实都是操作的该对象

Thread --> ThreadLocal.ThreadLocalMap threadLocals = null;),它是个什么呢?

ThreadLocalMap 其实是ThreadLocal的一个内部静态类:

3.2.1 field

//初始化容量大小 private static final int INITIAL_CAPACITY = 16; //存放数据的Entry 也就是ThreadLocal set 的值真正存储到了Entry[]中 private Entry[] table; //表中实际存储Entry的个数 private int size = 0; //重新分配表大小的阈值,默认为0 private int threshold; // Default to 0

3.2.2 Constructor

ThreadLocalMap 构造方法源码如下:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }

创建ThreadLocalMap对象时其实内部是创建了一个Entry对象:new Entry(firstKey, firstValue)

  • firstKey:ThreadLocal对象
  • firstValue: ThreadLocal set的时候传入的值

重新整理一下链路:

  1. ThreadLocal set()

    • set 方法中首先获取到当前线程

    • 获取当前线程的 threadLocals

    • 首次进来 threadLocals值为null

  2. createMap()

    • new ThreadLocal(ThreadLocal,Object)
  3. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

    • new Entry(ThreadLocal<?> k, Object v)

3.2.3 getEntry()

private Entry getEntry(ThreadLocal<?> key) { // * 使用当前的ThreadLocal实例 计算出数组index int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }

getEntry()时使用的是ThreadLocal实例作为key计算的数组index

3.2.4 set()

private void set(ThreadLocal<?> key, Object value) { //将 table 表赋给 tab Entry[] tab = table; //记录未设置值前 table 的长度 int len = tab.length; // * 使用当前的ThreadLocal实例 计算出数组index int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //得到当前 entry 的 key(TreadLocal 的弱引用) ThreadLocal<?> k = e.get(); //Threadloca1 对应的key存在,直接覆盖之前的值 if (k == key) { e.value = value; return; } //key为nu11 ,但是值不为nu11,说明之前的ThreadLocal 对象已经被回收了 if (k == null) { //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏 replaceStaleEntry(key, value, i); return; } } //ThreadLoca1对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。 tab[i] = new Entry(key, value); int sz = ++size; /** * cleanSomes1ots用于清除那些 e.get()==nu11 的元素, * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置nu11. * 如果没有清除任何 entry ,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作) */ if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

3.2.5 remove()

private void remove(ThreadLocal<?> key) { //将 table 表赋给 tab Entry[] tab = table; //记录未设置值前 table 的长度 int len = tab.length; // * 使用当前的ThreadLocal实例 计算出数组index int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//线性探测寻找 if (e.get() == key) { //清理失效的 key e.clear(); expungeStaleEntry(i); return; } } }

3.3 Entry

Entry 其实 是 ThreadLocalMap 的内部静态类。好一个套娃~ ThreadLocal ---> ThreadLocalMap ---> Entry

static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { //Entry 里的Key存放 ThreadLocal对象的是弱引用 super(k); value = v; } }

弱引用就是只要发生垃圾回收弱引用指向的对象就会被回收。

这里 Entry 对象继承了 WeakReference,通过super(k)可以了解到弱引用指向了ThreadLocal实例,大概关系是:

4. 内存分布

  1. 假设随着 Stack 的执行结束,ThreadLocal Ref 被GC回收了(假设使用的是线程池中的Thread对象)。

  2. ThreadLocalMap 中的 key只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

  3. 没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef-->currentThread-->threadLocalMap-->entry --> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏(常驻内存)。

原因
1.没有良好的编程思想,使用完未释放。

2.Thread对象 生命周期太长了。

参考:

https://blog.csdn.net/weixin_44075132/article/details/115543608


__EOF__

本文作者张铁牛
本文链接https://www.cnblogs.com/ludangxin/p/16537390.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   张铁牛  阅读(95)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示