ThreadLocal源码分析

简介: ThreadLocal,即线程局部变量。主要用于线程间数据隔离。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

一、概述
ThreadLocal,即线程局部变量。主要用于线程间数据隔离。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。
ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。
二、原理
ThreadLocal如何实现线程独立访问ThreadLocal关联的变量呢?
这里主要有两种方式:

在ThreadLocal中维护一个map,map的key是线程,value是关联的变量。但这种方式不太优雅(JDK1.5之前采用的这种方式),比如说可能会导致线程很大,而且当线程销毁时,还需要在map中将其删除,在多线程情形下,会增加维护难度和时间成本。
每个Thread维护一个ThreadLoaclMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样做有很多好处,比如不用加锁来保证读写安全,而且当线程销毁时,与其关联的ThreadLocalMap也自然消亡。

ThreadLocalMap的说明
ThreadLocal没有直接使用HashMap而是自己重新开发了一个map,最主要的作用是让他的key为虚引用类型,这样当ThreadLocal对象销毁时,多个持有其引用的线程不会影响它的回收。
ThreadLocalMap是一个很像HashMap的一个数据结构,但他并没有实现Map接口,而且它的Entry是继承WeakReference的,也没有next指针,所以不存在链表了。对于hash冲突,而是采用的开放地址法来进行解决
ThreadLocaMap的扩容机制也不同于HashMap,ThreadLocalMap的扩容阈值是长度的2/3,当表中的元素数量达到阈值时,不会立即进行扩容,而是会触发一次rehash操作清除过期数据,如果清除过期数据之后元素数量大于等于总容量的3/4才会进行真正意义上的扩容
// ThreadLocalMap没有继承Map接口
static class ThreadLocalMap{

// Entry被声明为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

}

// 调用rehas方法
private void rehash() {

// 清除过期数据
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
// 清除过期数据后数据仍然大于等于总容量的3/4,则扩容
if (size >= threshold - threshold / 4)
    resize();

}

复制代码
get/set/初始化
当调用get或set方法时,首先会去检查线程的ThreadLocalMap是否被初始化,如果没有初始化,则会进行初始化操作,否则根据计算出来的key找到对应下标,如果对应下标是我们要找的元素,则返回,否则会向后查找,直到碰到slot为null或者找到为止。在这过程中同时会清理过期的K-V对,set同理。具体可以参看源码。
// get方法
public T get() {

// 获取当前线程类
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 判断是否初始化,如果没有则开始初始化
if (map != null) {
    // 如果当前下标是目标元素,则返回,否则调用getEntryAfterMiss方法查找
    // getEntryAfterMiss方法会继续查找并清理过期数据
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
        @SuppressWarnings("unchecked")
            T result = (T)e.value;
        return result;
    }
}
return setInitialValue();

}

// 初始化
private T setInitialValue() {

// 默认返回null,可以重新此方法决定每个线程在初始化map时获取的值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
    map.set(this, value);
} else {
    createMap(t, value);
}
....
return value;

}

// set方法
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

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();
    // 如果key相同,直接替换就行
    if (k == key) {
        e.value = value;
        return;
    }
    // 过期数据,需要清理
    // replaceStaleEntry函数逻辑:向后查找,直到遇到目标元素更新数据或桶位为空
    // 插入值并清除过期数据
    if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
    }
}
// 最后插入我们的目标元素
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

}
复制代码
三、内存泄漏
强引用内存泄漏
如果Entry的key为强引用,则会导致ThreadLocal实例在被创建它的线程销毁时,而无法被回收,从而导致严重的内存泄漏问题,因此Eetry的key被声明为弱引用来避免这种问题
弱引用内存泄露
我们知道每一个线程都存在一个ThreadLocalMap,Map中的key为一个ThreadLocal实例。而且key到ThreadLocal实例的引用为虚引用,也就是说当ThreadLocal置为null时,没有任何强引用指向ThreadLocal实例,所以ThreadLocal实例会被GC回收。但是value却不能被回收,因为存在一条从当前线程连接过来的强引用(Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value)
针对上面的内存泄露问题,ThreadLocal在get和set时都会检测并清除key为null的Entry,从而尽可能的避免内存泄露

使用建议

每次使用完ThreadLocal,都调用它的remove()方法,清除数据
将ThreadLocal声明为private static,使它的生命周期与线程保持一致

四、总结
本文简单介绍了ThreadLocal相关的原理并分析了部分主要源码,如有错误,还望不吝指正,感谢~

相关文章
|
5月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
63 0
|
2月前
|
存储 设计模式 安全
深入理解ThreadLocal原理
本文深入探讨了Java中的ThreadLocal及其内部数据结构ThreadLocalMap的工作原理和特性,帮助读者理解如何利用ThreadLocal实现线程局部变量的隔离和线程安全。
深入理解ThreadLocal原理
|
3月前
|
存储 算法 Java
ThreadLocal 源码浅析
【7月更文挑战第5天】`ThreadLocal` 是Java中用于创建线程局部变量的工具类,确保每个线程拥有独立的变量副本。源码中,每个`ThreadLocal`实例都有一个唯一的哈希码用于映射到`ThreadLocalMap`,这个内部静态类使用弱引用存储键(`ThreadLocal`实例)以防止内存泄漏。`ThreadLocalMap`使用 Entry 数组,Entry 是一个扩展了 WeakReference 的类,持有线程变量值。`ThreadLocal`的`get()`和`set()`方法通过哈希计算定位并访问或设置线程局部变量。
|
5月前
|
存储 安全 Java
ThreadLocal原理讲解
ThreadLocal原理讲解
46 0
|
存储 SpringCloudAlibaba Java
浅析ThreadLocal使用及实现原理
提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其`get` 或 `set`方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。`ThreadLocal`实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联 。所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以这么说Th
98 0
浅析ThreadLocal使用及实现原理
|
存储 算法 Java
ThreadLocal原理解析
ThreadLocal原理解析
99 1
ThreadLocal原理解析
|
存储 算法 安全
ThreadLocal原理剖析
ThreadLocal原理剖析
206 0
|
Java 定位技术
ThreadLocal原理
经典八股文之ThreadLocal原理
182 0
|
存储 Java
ThreadLocal的使用及原理解析
JDK的lang包下提供了ThreadLocal类,我们可以使用它创建一个线程变量,线程变量的作用域仅在于此线程内
128 0