ThreadLocal简介
那么首先,我们需要了解一下什么是ThreadLocal呢?以下是官方文档对ThreadLocal的简要介绍:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
此类提供线程局部变量,这些变量不同于普通变量,因为访问这些变量的每个线程(通过get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是希望将类中的私有静态字段与线程(例如,用户ID或事务ID)关联的。
只要线程处于活动状态且ThreadLocal实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;线程消失后,其线程本地实例的所有副本都将接受垃圾收集(除非存在对这些副本的其他引用)。
转换成通俗的话来说就是,ThreadLocal对每个线程都保存了一份单独的变量副本,这样不同线程都只需对自身变量操作。从而避免了需要进行同步等复杂的步骤。(很豪横的思想,只要每个线程都有变量,不就不用做同步那些复杂的操作了么?!)
了解完ThreadLocal是什么了,那么什么情况下适合使用ThreadLocal进行开发呢?
基于ThreadLocal本身的特性,其主要适用于一些多线程下修改变量,但不希望变量间互相影响的场景。
举项目中最常见的一个用法,就是用于保存前端请求的请求头信息。
因为通常情况下,每次前端的请求服务器都会新起一个线程用于处理及传输,那么此时请求中的header头信息,肯定是只属于这个线程的,基于这个点,很容易就能和Thread Local本身以线程进行隔离的特性结合起来。
这样的话,能保证每个线程拿到的都会是自己对应的前端请求的请求头信息。
当然还有一些别的场景,比如如何区别当前请求是压测请求还是正常请求,也都可以采用ThreadLocal进行实现。
ThreadLocal原理分析
那么,ThreadLocal底层又是如何实现的这样的结构以支持其针对每个线程都能有其唯一的变量呢?接下来就由我来带你们一看源码底层是如何实现的。
get方法
首先我们来看get方法,其源代码如下所示:
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 取到对应线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 从这个map中取到对应的Entry结构
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; // 泛型编程,转换成对应需要的类型。
return result;
}
}
return setInitialValue();
}
可以看到,比较关键的点就是ThreadLocalMap及其对应的Entry结构。那么这两个又是什么东西呢?
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
// value 对应的其实是Thread在该ThreadLocal下的值。
value = v;
}
}
......
private Entry[] table;
......
可以看到,实际上ThreadLocalMap就是一个Entry数组。而每个Entry对应的是线程在一个ThreadLocal下的值。这样设计的原因就是为了充分兼顾一个线程可能存在的多个ThreadLocal的情况。具体图示如下所示:
那么如此一来,我们就可以梳理下从ThreadLocal中获取值的逻辑了:
1、获取到当前线程Thread
2、根据Thread获取到对应的ThreadLocalMap
3、从ThreadLocalMap中再获取当前ThreadLocal对应的Entry,并返回Entry中的值即可。
内存泄露
需要注意的一点小细节是,ThreadLocalMap中的Entry对象都是采用的弱引用。而这可能会导致一个比较严重问题,产生内存泄露。
为什么会产生内存泄露呢?这里我先借用一个Threadlocal引用的大致图解:
可以看到如果此时外界对ThreadLocal的引用取消了,那么此时ThreadLocal因为是弱引用,就会被GC进行回收,从而,我们就再也没有办法获取到对应的value值了,从而造成了内存泄露。
那么为什么要使用弱引用呢?改成强引用是否能解决问题呢?假设ThreadLocal使用的是强引用,那么当对ThreadLocal的引用被取消后,ThreadLocal由于依旧被ThreadLocalMap所指向,因此没法进行回收。
那么还是会造成内存泄露的问题,甚至泄漏的更“多”。
那么明显本质上,弱引用并不是引起内存泄漏的原因。实际上,要避免出现内存泄漏,还是需要我们线程主动的去"切断"引用。也即是在每次使用完后,都去调用相应的remove方法进行清除。
set方法
再看set方法,其实逻辑也是类似的。源代码我先放在这里了:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到也是
1、首先获取当前线程Thread。
2、获取到map对应的ThreadLocalMap对象。
3、判断当前ThreadLocalMap是否已经被创建,如果已经创建则往其中塞入值(key是ThreadLocal,value就是我们set的值)。
4、否则的话进行map的初始化。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; // 初始化数组,长度默认16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 计算对应的哈希下标
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY); // 设置扩容阈值
}
初始化的关键代码我也放在这里了~如果上面的知识你都能了解的差不多的话,那么这里的初始化也必然不在话下啦~。
ThreadLocal优劣分析
优点
1、针对于常见的加锁保证互斥解决多线程访问的问题,ThreadLocal提出了新的思路,采用给每个线程都分配变量的方法解决冲突。性能相对较好。
2、各线程对于变量数据的修改是独立的,不会互相影响。
缺点
1、需要注意使用后调用remove方法清除对应的引用,避免出现内存泄漏的问题。
2、由于ThreadLocal作用的机理与线程是绑定的,因此不能直接用于保存异步任务的数据,需要通过一些别的方式进行操作。关于具体怎么实现,请期待我后续的文章哈哈哈~
如果你看到了这里,不妨给我点个赞、点个收藏,要是还能关注一下下就更好啦~
创作不易,感谢支持~