ThreadLocal 源码剖析

1,124 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

ThreadLocal概述

ThreadLocal 见名知意:“线程变量”,即它为每个使用该变量的线程都提供了一个独有的副本,互不影响且决了线程并发访问的冲突。

  1. 线程并发:在多线程并发场景下使用。

  2. 传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。

  3. 线程隔离:每个线程变量都是独立的,不会相互影响。

我接触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;
}

应用

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…