作用
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
敖丙举的例子:
抽出对象做线程隔离会比传给每个方法要好许多
源码
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
set源码很简单,其实ThreadLocal就是一个map,key是该线程对象,value是set的值
getMap方法取的是线程类自己的threadLocals,从而做到了数据的隔离
ThreadLocalMap的entry实现有点不一样,他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
结构如图
实现用数组不用的原因:用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
换言之,一个线程大不了new多个TreadLocal来避免产生hash冲突
如果产生hash冲突的话,看一下set源码
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
通过int i = key.threadLocalHashCode & (len-1)
算出hash值来定位到table中的位置i
如果当前位置是空的,就初始化一个Entry对象放在位置i上;
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
总计:就是先看hash冲突,冲突的话再判断线程,线程如果相同则替换,不相同就再寻找下一个为空的位置
set和get如果冲突严重的话,效率还是很低的。
get的源码
private Entry getEntry(ThreadLocal<?> key) {
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);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找到相等位置。
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
JVM中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
但ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
如果我想共享线程的ThreadLocal数据怎么办?
关于InheritableThreadLocal
我之前也写过一篇,可以看这个:
三分钟搞懂InheritableThreadLocal以及实战父子线程间的共享数据
其实就是thread中还有另一个变量
全局搜一下代码只有这里调到了
因为父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals就ok了
内存泄露问题
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
解决办法
这里写的比较简单,详细可以看这一篇:
为何每次用完 ThreadLocal 都要调用 remove()?——内存泄漏
在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了
但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏
项目中使用的场景
我们用到的地方像是动态数据源的配置
配置相应的数据源后set
get和remove的封装
试一下
用到的场景就是saas情况下数据源(db,mq)的切换
remove的使用,确保不会内存泄露(这里的clean方法其实就是直接封装remove)
参考:
拼多多面试官没想到ThreadLocal我用得这么溜,人直接傻掉
为何每次用完 ThreadLocal 都要调用 remove()?——内存泄漏
juc的复习:
java–JUC快速入门(彻底搞懂JUC)