ThreadLocal设计的目的
按照jdk源码的注释,ThreadLocal用于保存线程本地变量。区别于普通变量是所有线程共享的,ThreadLocal保存的变量不是共享的,每个线程有自己独有的一份(语句不严格,不过大家应该了解其含义)。
ThreadLocal使用的场景
根据ThreadLocal的特性,一般ThreadLocal用于保存线程上下文。说通俗一点就是线程独有,而且代码全局可访问。
解释:
1.线程独有,如果不是线程独有,使用普通变量即可。
2.全局可访问,如果不是全局访问,那变量肯定是函数中的临时变量,函数执行过程中的临时变量是在本线程私有的栈上面存储的,不会被共享,使用普通变量也没有关系。
应用示例:
1. spring事务,事务的开启和提交不在一个函数,但是两个操作必须是同一个connection,不然会提交错误的事务。这样数据库连接可以保存到ThreadLocal中,整个线程都可以访问到唯一且一致的connection。
2. 比如大规模系统中,日志都会被收集到ES。为了查询单次请求所有日志,我可以创建一个Filter,进来的时候使用ThreadLocal保存一个UUID,后续打日志的时候,统一加上这个UUID。这样通过该关键字查询日志系统,就可以把单次请求的日志按顺序输出,方便查阅。
ThreadLocal源码解读
public static void main(String[] args) {
ThreadLocal<String> a = new ThreadLocal<>();
a.set("aa");
Thread current = Thread.currentThread();
}
以该段代码为例,内存结构如下:
说明:
1.线程内部有个字段为threadLocals,类型为ThreadLocalMap;该类型类似Map,用于存储key-value结构。Key为ThreadLocal变量,如示例代码中的变量a,value为ThreadLocal.set()方法传递的值,如示例代码中的"aa";
2.ThreadLocal.set()方法实际上是将值存入了当前线程threadLocals中。代码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap中的Entry是WeakReference类型
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap设计解析
名词说明:
本地变量:ThreadLocal.set()方法设置的值,或者说ThreadLocal对象持有的值。
ThreadLocal主要用于保存线程本地变量,所以肯定有个Map存储三者(线程、ThreadLocal、本地变量值)中两者的关系。如果Map放在ThreadLocal中,则(key,value)为(线程、本地变量),如果Map放在线程中,(key,value)为(ThreadLocal、本地变量)。
Map中的数据不主动remove,很容易产生内存泄漏。现实中应该有很多这种事故,变量存入集合中,使用完后忘记清理集合导致内存泄漏。
为了解决这个问题,jdk设计的ThreadLocal体系,不用大家显式去释放。
1. ThreadLocalMap中的Entry是WeakReference类型,其"弱引用"ThreadLocal对象。这样当其他地方对ThreadLocal对象的引用解除后,其内存会被回收,Entry中的key被设置成null。这样就有机制感知到能够被回收的本地变量。
2. 如果线程结束,线程里面的ThreadLocalMap字段会被回收。这样线程本地变量会被回收,不会影响其他线程正常访问ThreadLocal。
3. 当前线程新创建ThreadLocal变量并调用其set方法时,会进入到ThreadLocalMap的set方法中。该方法在存储完key-value后会对失效的ThreadLocal进行清理,防止内存泄漏。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
# 计算hash索引
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
# 使用开放地址法解决哈希碰撞,而不是常规的链表
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
# 因为没有创建新的Entry,不会增加内存,所以不会去清理无效的ThreadLocal
if (k == key) {
e.value = value;
return;
}
# k == null,说明该Entry之前存储的ThreadLocal已经失效
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
# 清理无效的ThreadLocal
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从代码中可以看到以下设计:
1.使用的时开放地址法解决哈希碰撞,而不是常规的链表。开放地址法内存利用率更高,一般ThreadLocal变量不会大规模使用,所以使用的时开放地址法能够有效节省内存。
2.set方法也不是每次都会清理无效的ThreadLocal,而是由新增Entry时才会触发清理。这样兼顾了性能和内存占用。因为不新增Entry,不会造成内存增加,没必要实时去清理失效的ThreadLocal。