ThreadLocal
1. 简介
ThreadLocal是Thread的局部变量,用于编写多线程程序,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。
能干什么:
- 存储数据 & 线程间数据隔离
- 在进行对象间跨层传递的时候,使用ThreadLocal可以避免多次传递。比如将用户信息
set
到ThreadLocal中,当前线程在任何地方都可以通过ThreadLocal对象直接get
用户信息,不用为了获取用户信息而把用户信息传来传去
2. 初探
既然说 ThreadLocal 是线程的局部变量
,那么一起验证下以下两个问题:
- 主线程中set的
Hello World ~
,在thread-1线程中能否get到 - 在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);
}
- 获取当前线程
- 获取Map
- 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
方法逻辑还是比较清晰的
- 获取当前线程
- 获取当前线程的
threadLocals
属性值作为操作的map - 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();
}
- 获取当前线程 & 返回当前线程的
threadLocals
作为map - map不为空就根据this(当前的ThreadLocal实例对象)获取到Map的Entry对象
- 如果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的时候传入的值
重新整理一下链路:
-
ThreadLocal set()
-
set 方法中首先获取到当前线程
-
获取当前线程的
threadLocals
值 -
首次进来
threadLocals
值为null
-
-
createMap()
- new ThreadLocal(ThreadLocal,Object)
-
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. 内存分布
-
假设随着 Stack 的执行结束,ThreadLocal Ref 被GC回收了(假设使用的是线程池中的Thread对象)。
-
ThreadLocalMap 中的 key只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
-
没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef-->currentThread-->threadLocalMap-->entry --> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏(常驻内存)。
原因
1.没有良好的编程思想,使用完未释放。
2.Thread对象 生命周期太长了。
参考:
https://blog.csdn.net/weixin_44075132/article/details/115543608