⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7395024434681937971 「想打游戏的程序猿」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

介绍

ThreadLocal 中设置的值仅属于当前线程,该值对其他线程而言是隔离的,所以在同一时间并发修改一个属性的值也不会互相影响。

使用

在使用 ThreadLocal 时,可以直接通过set(T value) 、get() 来设置 threadLocal 的值、获取 threadLocal 的值。

set 方法

public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) { // 如果map不是空
map.set(this, value); // 设置值
} else {
createMap(t, value); // 创建并设置值
}
}

// 获取线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

// 对该ThreadLocal设置值
private void set(ThreadLocal<?> key, Object value) {

// ThreadLocalMap内部的table数组
Entry[] tab = table;
int len = tab.length;
// 根据threadLocal的hash和长度进行与运算,找到下标位置
int i = key.threadLocalHashCode & (len-1);

// 曾经该threadLocal有值,设置值并返回
for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
// 获取entry的引用
ThreadLocal<?> k = e.get();
// 引用等于当前threadLocal 则进行设置值
if (k == key) {
e.value = value;
return;
}
// 当前引用为空,把key、value组装成entry放到i位置上,并清楚key为空的entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

// 组装entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 如果没有元素被清楚,并当前数组大小大于threshold则进行rehash;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

其中 threshold = len * 2 / 3,它是通过setThreshold方法进行设置的。而每次rehash的时候都会调用resize方法,它会读取 oldTable.length,把newLen设置为oldLen的两倍。

这里有一个注意点int i = key.threadLocalHashCode & (len-1);下标是通过 hash 来确定的,会出现 hash 冲突,这里采用的是开放地址法来解决 hash 冲突,在下面的代码中有判断k==key,如果不相等则nextIndex(i, len)获取下一个下标来判断。

上述就是整个set的过程,下面来看一下get

public T get() {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this为当前threadLocal,获取对应的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 返回当前entry的值即可。
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始值并返回,初始值是null
return setInitialValue();
}


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
// 开始遍历entry数组,如果能找到key的entry就返回否则返回null
return getEntryAfterMiss(key, i, e);
}

get方法要比set简单很多,只是根据 key 找对应 entry,把 entry 的值返回即可。

结构

通过上述源码,可以总结出 threadLocal 的数据结构如下:

问题

根据上面的介绍,可以看出一些潜在的问题;例如在使用 threadLocal 时堆栈信息如下:

真的会内存泄漏?

当使用完 threadLocal,threadLocal 的对象引用就不存在了,而 key 对 threadLocal 是弱引用,gc 后这段引用也不存在了。此时无法通过map.getEntry(this)找到对应的 entry,而 entry 还一直存在 Entry[]中,就有可能导致了内存溢出。这里我写了是有可能导致内存溢出,例如在set方法中有这样一行代码

if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();

该方法的具体代码如下:

private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}

当有新的 threadlocal 进行设置值时都会进行清除一下e.get() == null引用为空的 Entry,而进入到这里的条件是(n >>>= 1) != 0,当长度为 16(10000)会触发 5 次,挨着当前 threadlocal 的 Entry 的连续 5 个都没有引用为 null 的话,就不会继续往下移除了。

所以如果频繁的调用set方法,它也会帮助清除一些之前 key 已经被 gc 掉的 entry 对象,但无论如何如果没有 gc 和调用set方法的话,这些 entry 对象会一直在内存中占用。

所以每次在使用完 threadlocal 时要调用一下remove方法,它会自动把 entry 移除。

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

除此之外在 threadlocal 时,尽量把它设置为 pricate static 变量,这样因为 threadLocal 的强引用一直存在,不会被垃圾回收掉这样就能保证任何时间都可以找到 Entry,并对其进行remove。

Entry 的 key 设置为强引用可以么?

当 ThreadLocal 的引用在用户栈中已经移除了,并且没有调用remove方法;但是 entry 还有一个强引用指向 threadLocal 对象,e.get()永远都不会是空,此时 entry 对象就永远无法被回收掉了。

这样弱引用比强引用就多一层保障弱引用的 ThreadLocal 会被回收,对应 value 在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。

示例:

public static void main(String[] args) {
for (int i = 0 ; i < 100 ; i ++){
ThreadLocal temp = new ThreadLocal();
temp.set(i);
temp = null;
}
// System.gc();
ThreadLocal m = new ThreadLocal();
m.set("value");
}

感兴趣的话,可以用上述示例跟着源码跑一遍源码的流程,当开启System.gc();时可以走到清理回收阶段。

子线程可以使用父线程的 threadLocal 中的值么?

不可以,如果想使用的话可以采用InheritableThreadLocal,它会在初始化子线程时进行设置子线程的 threadlocal,也仅仅在初始化时有关联,后续子线程和父线程互相更改 threadlocal 都不会有任何影响。示例:

private static InheritableThreadLocal threadLocal = new InheritableThreadLocal();
@SneakyThrows
public static void main(String[] args) {
threadLocal.set("1");
Thread thread = new Thread(
() -> {
System.out.println("子线程获取threadLocal的值为:" + threadLocal.get());
threadLocal.set("2");
}
);
thread.start();
Thread.sleep(200);
System.out.println("父线程获取threadLocal的值为:" + threadLocal.get());
}

1、父线程先设置 threadLocal 的值为 1;

2、开启一个子线程,获取 threadLocal 的值,得到结果为 1;

3、子线程设置 threadLocal 为 2,并且 get 一下,得到的结果为 2;

4、睡眠 200ms 确保子线程命令都执行完成;

5、父线程获取 threadLocal 的值,得到的结果为 1。

文章目录
  1. 1. 介绍
  2. 2. 使用
    1. 2.1. set 方法
    2. 2.2. 结构
  3. 3. 问题
    1. 3.1. 真的会内存泄漏?
    2. 3.2. Entry 的 key 设置为强引用可以么?
    3. 3.3. 示例:
    4. 3.4. 子线程可以使用父线程的 threadLocal 中的值么?