开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
ThreadLocal概述
ThreadLocal 见名知意:“线程变量”,即它为每个使用该变量的线程都提供了一个独有的副本,互不影响且决了线程并发访问的冲突。
-
线程并发:在多线程并发场景下使用。
-
传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。
-
线程隔离:每个线程变量都是独立的,不会相互影响。
我接触ThreadLocal最多的就是上下文数据的访问:
public class ContextUtil{
private static final NamedThreadLocal<RequestInfo> REQUEST_INFO_THREAD_LOCAL = new NamedThreadLocal<>("requestInfo");
...
}
这样可能你就有疑问了,“那我们用的Synchronized 关键字不也是为了解决线程并发访问的冲突么?他俩有什么区别么?”
代码说明
public class ThreadLocalDemo{
private static ThreadLocal<String> strThreadLocal = new ThreadLocal<>;
private static ThreadLocal<Integer> intThreadLocal = new ThreadLocal<>;
}
- 同一个线程中的ThreadLocalMap中国有两个key,分别是 strThreadLocal队形和intThreadLocal对象,但是他们属于同一个ThreadLocalMap对象
Synchronized和ThreadLocal的区别
Synchronized虽然能起到相同的作用,但是它消耗的时间即降低时间来解决访问冲突
而ThreadLocal是通过每个线程单独一份存储空间、降低空间利用率、消耗内存来解决访问冲突
两者重要的区别在此,这也同样决定了两者在应用场景会有大的不同
ThreadLocal原理
每个Thread内部都有一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,里面用Entry存储着当前ThreadLocal对象的key和对应的值,所以我们调动ThreadLocal的set(T)方法的时候,其实只是把当前的ThreadLocal对象作为key,把value放在每个线程的ThreadLocalMap中,ThreadLocal其实就是把每个变量在不同的线程中都创建了一个副本,正因为如此,ThreadLocal才能够起到内存隔离。
set方法
首先获取线程,然后获取线程的Map。如果Map不为空则将当前ThreadLocal的引用作为key设置到Map中。如果Map为空,则创建一个Map并设置初始值。
get方法
首先获取当前线程,然后获取Map。如果Map不为空,则Map根据ThreadLocal的引用来获取Entry,如果Entry不为空,则获取到value值,返回。如果Map为空或者Entry为空,则初始化并获取初始值value,然后用ThreadLocal引用和value作为key和value创建一个新的Map。
remove方法
删除当前线程中保存的ThreadLocal对应的实体entry。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
initialValue方法
该方法的第一次调用发生在当线程通过get方法访问线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue才不会被这个线程调用。每个线程最多调用一次这个方法。
该方法只返回一个null,如果想要线程变量有初始值需要通过子类继承ThreadLocal的方式去重写此方法,通常可以通过匿名内部类的方式实现。这个方法是protected修饰的,是为了让子类覆盖而设计的。
protect T initialValue(){
return null;
}
应用
-
spring中的@Transcation注解
Page Helper使用ThreadLocal的原理:在你要使用分页查询的时候,先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用mybatis提供的拦截器(插件)实现一个com.github.pagehelper.PageInterceptor接口,这个分页拦截器拦截到后会从ThreadLocal中拿到分页的信息,如果有分页信息,这进行分页查询,最后再把ThreadLocal中的东西清除掉。
ThreadLocal的内存泄漏
当然,ThreadLocal也有弊端,就是容易造成内存泄漏
-
内存泄漏
:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等验证后沟。内存泄漏的堆积会导致内存溢出。 -
内存溢出
:没有足够的内存供申请者提供
细心的朋友们可能发现了ThreadLocalMap这个类的K是一个WeakReference
(弱引用)包括的泛型
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用
:垃圾回收器一旦发现了弱引用的对象,不管内存是否足够,都会回收它的内存。
因为线程的生命周期是很长的,你的项目不崩的话线程可能一直存在。
每个线程内都有个ThreadLocalMap,所以说ThreadLocalMap的生命周期也是同样长。
如果这个K是强引用,那么就无法被回收,最终就会造成内存泄漏。
当然现在使用ThreadLocal真正造成你内存泄漏的是没有及时的remove()
,因为ThreadLocalMap中使用的key为ThreadLocal的弱引用,但是value是强引用,如果你不remove的话,value也是不会被GC清理的。
// ThreadLocal.remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// ThreadLocalMap
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
这个其实也被优化了,如果你忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收,对应的value会在下一次ThreadLocalMap调用get、set、remove方法的时候被清除,从而避免了内存泄漏。你说体贴不体贴。
ThreadLocalMap解决Hash冲突
ThreadLocalMap构造方法
构造函数创建一个长队为16的Entry数组,然后计算firstKey的索引,存储到table中,设置size和threshold。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用来计算索引,nextHashCode是Atomicinteger类型的,Atomicinteger类是提供原子操作的Integer类,通过线程安全的方式来加减,适合高并发使用。
每次在当前值上加上一个HASH_INCREMENT值,这个值和斐波拉契数列有关,主要目的是为了让哈希码可以均匀的分布在2的n次方的数组里,从而尽量的避免冲突。
当size为2的幂次的时候,hashCode & (size - 1)相当于取模运算hashCode % size,位运算比取模更高效一些。为了使用这种取模运算, 所有size必须是2的幂次。这样一来,在保证索引不越界的情况下,减少冲突的次数。
ThreadLocalMap的set方法
ThreadLocalMao使用了线性探测法来解决冲突。线性探测法探测下一个地址,找到空的地址则插入,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当前key的hash值是6,6的位置已经被占用了,则hash值加一,寻找7的位置,7的位置也被占用了,回到0的位置。直到可以插入为止,可以将这个数组看成一个环形数组。
(下篇内容详细给大家说下强软弱虚
四种引用的区别和作用,这里埋个土哈。)
InheritableThreadLocal
有些场景会用到主线程的线程变量要让子线程也能狗访问,这个时候就用到 InheritableThreadLocal 这个类了
原理:
子线程在创建的时候 new Thread() 调用 init() 方法
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
这里会把主线程的 inheritableThreadLocals 的值的引用复制给子线程
真实案例
grpc 中 io.grpc.Context 在 多线程的上下文数据不共享,原因:Context底层用的是ThreadLocal ,所以线程间的变量不共享
解决方案,使用 Context.attach() 和 Context.detach() + TaskDecorator 在线程池中使用 具体看 crm.membership 项目
TaskDecorator 相关解读: blog.csdn.net/qq_29569183…
threadlocal变量透传 解读:cloud.tencent.com/developer/a…